Commit 69a41f07 authored by Christian Sadilek's avatar Christian Sadilek
Browse files

Closes #4681: Support observing engine state changes for web extensions

parent a2536950
......@@ -24,7 +24,7 @@ import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.utils.EngineVersion
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import org.json.JSONObject
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlockingController
......@@ -49,8 +49,8 @@ class GeckoEngine(
TrackingProtectionExceptionFileStorage(context, runtime)
) : Engine {
private val executor by lazy { executorProvider.invoke() }
private val localeUpdater = LocaleSettingUpdater(context, runtime)
private var webExtensionDelegate: WebExtensionDelegate? = null
init {
runtime.delegate = GeckoRuntime.Delegate {
......@@ -139,6 +139,7 @@ class GeckoEngine(
) {
GeckoWebExtension(id, url, allowContentMessaging).also { ext ->
runtime.registerWebExtension(ext.nativeExtension).then({
webExtensionDelegate?.onInstalled(ext)
onSuccess(ext)
GeckoResult<Void>()
}, {
......@@ -149,11 +150,13 @@ class GeckoEngine(
}
/**
* See [Engine.registerWebExtensionTabDelegate].
* See [Engine.registerWebExtensionDelegate].
*/
override fun registerWebExtensionTabDelegate(
webExtensionTabDelegate: WebExtensionTabDelegate
override fun registerWebExtensionDelegate(
webExtensionDelegate: WebExtensionDelegate
) {
this.webExtensionDelegate = webExtensionDelegate
val tabsDelegate = object : WebExtensionController.TabDelegate {
override fun onNewTab(
webExtension: org.mozilla.geckoview.WebExtension?,
......@@ -161,7 +164,7 @@ class GeckoEngine(
): GeckoResult<GeckoSession>? {
val geckoEngineSession = GeckoEngineSession(runtime, openGeckoSession = false)
val geckoWebExtension = webExtension?.let { GeckoWebExtension(it.id, it.location) }
webExtensionTabDelegate.onNewTab(geckoWebExtension, url ?: "", geckoEngineSession)
webExtensionDelegate.onNewTab(geckoWebExtension, url ?: "", geckoEngineSession)
return GeckoResult.fromValue(geckoEngineSession.geckoSession)
}
......
......@@ -17,7 +17,8 @@ import mozilla.components.concept.engine.UnsupportedSettingException
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
......@@ -531,20 +532,20 @@ class GeckoEngineTest {
val webExtensionController: WebExtensionController = mock()
whenever(runtime.webExtensionController).thenReturn(webExtensionController)
val webExtensionsTabDelegate: WebExtensionTabDelegate = mock()
val webExtensionsDelegate: WebExtensionDelegate = mock()
val engine = GeckoEngine(context, runtime = runtime)
engine.registerWebExtensionTabDelegate(webExtensionsTabDelegate)
engine.registerWebExtensionDelegate(webExtensionsDelegate)
val captor = argumentCaptor<WebExtensionController.TabDelegate>()
verify(webExtensionController).tabDelegate = captor.capture()
val engineSessionCaptor = argumentCaptor<GeckoEngineSession>()
captor.value.onNewTab(null, null)
verify(webExtensionsTabDelegate).onNewTab(eq(null), eq(""), engineSessionCaptor.capture())
verify(webExtensionsDelegate).onNewTab(eq(null), eq(""), engineSessionCaptor.capture())
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
captor.value.onNewTab(null, "https://www.mozilla.org")
verify(webExtensionsTabDelegate).onNewTab(eq(null), eq("https://www.mozilla.org"),
verify(webExtensionsDelegate).onNewTab(eq(null), eq("https://www.mozilla.org"),
engineSessionCaptor.capture())
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
......@@ -552,12 +553,23 @@ class GeckoEngineTest {
val webExt = org.mozilla.geckoview.WebExtension("test")
val geckoExtCap = argumentCaptor<mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension>()
captor.value.onNewTab(webExt, "https://test-moz.org")
verify(webExtensionsTabDelegate).onNewTab(geckoExtCap.capture(), eq("https://test-moz.org"),
verify(webExtensionsDelegate).onNewTab(geckoExtCap.capture(), eq("https://test-moz.org"),
engineSessionCaptor.capture())
assertNotNull(geckoExtCap.value)
assertEquals(geckoExtCap.value.id, webExt.id)
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
// Verify we notify onInstalled
val result = GeckoResult<Void>()
whenever(runtime.registerWebExtension(any())).thenReturn(result)
engine.installWebExtension("test-webext", "resource://android/assets/extensions/test")
result.complete(null)
val extCaptor = argumentCaptor<WebExtension>()
verify(webExtensionsDelegate).onInstalled(extCaptor.capture())
assertEquals("test-webext", extCaptor.value.id)
assertEquals("resource://android/assets/extensions/test", extCaptor.value.url)
}
@Test(expected = RuntimeException::class)
......
......@@ -24,7 +24,7 @@ import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.utils.EngineVersion
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import org.json.JSONObject
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlockingController
......@@ -50,6 +50,7 @@ class GeckoEngine(
) : Engine {
private val executor by lazy { executorProvider.invoke() }
private val localeUpdater = LocaleSettingUpdater(context, runtime)
private var webExtensionDelegate: WebExtensionDelegate? = null
init {
runtime.delegate = GeckoRuntime.Delegate {
......@@ -138,6 +139,7 @@ class GeckoEngine(
) {
GeckoWebExtension(id, url, allowContentMessaging).also { ext ->
runtime.registerWebExtension(ext.nativeExtension).then({
webExtensionDelegate?.onInstalled(ext)
onSuccess(ext)
GeckoResult<Void>()
}, {
......@@ -148,11 +150,13 @@ class GeckoEngine(
}
/**
* See [Engine.registerWebExtensionTabDelegate].
* See [Engine.registerWebExtensionDelegate].
*/
override fun registerWebExtensionTabDelegate(
webExtensionTabDelegate: WebExtensionTabDelegate
override fun registerWebExtensionDelegate(
webExtensionDelegate: WebExtensionDelegate
) {
this.webExtensionDelegate = webExtensionDelegate
val tabsDelegate = object : WebExtensionController.TabDelegate {
override fun onNewTab(
webExtension: org.mozilla.geckoview.WebExtension?,
......@@ -160,7 +164,7 @@ class GeckoEngine(
): GeckoResult<GeckoSession>? {
val geckoEngineSession = GeckoEngineSession(runtime, openGeckoSession = false)
val geckoWebExtension = webExtension?.let { GeckoWebExtension(it.id, it.location) }
webExtensionTabDelegate.onNewTab(geckoWebExtension, url ?: "", geckoEngineSession)
webExtensionDelegate.onNewTab(geckoWebExtension, url ?: "", geckoEngineSession)
return GeckoResult.fromValue(geckoEngineSession.geckoSession)
}
......
......@@ -18,7 +18,8 @@ import mozilla.components.concept.engine.UnsupportedSettingException
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
......@@ -419,64 +420,6 @@ class GeckoEngineTest {
assertFalse(onErrorCalled)
}
@Test
fun `fetch trackers logged successfully`() {
val runtime = mock<GeckoRuntime>()
val engine = GeckoEngine(context, runtime = runtime)
var onSuccessCalled = false
var onErrorCalled = false
val mockSession = mock<GeckoEngineSession>()
var trackersLog: List<TrackerLog>? = null
val mockContentBlockingController = mock<ContentBlockingController>()
var logEntriesResult = GeckoResult<List<ContentBlockingController.LogEntry>>()
whenever(runtime.contentBlockingController).thenReturn(mockContentBlockingController)
whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
engine.getTrackersLog(
mockSession,
onSuccess = {
trackersLog = it
onSuccessCalled = true
},
onError = { onErrorCalled = true }
)
logEntriesResult.complete(createDummyLogEntryList())
val trackerLog = trackersLog!!.first()
assertTrue(trackerLog.cookiesHasBeenBlocked)
assertEquals("www.tracker.com", trackerLog.url)
assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES))
assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.FINGERPRINTING))
assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.CRYPTOMINING))
assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES))
assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.FINGERPRINTING))
assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.CRYPTOMINING))
assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
assertTrue(onSuccessCalled)
assertFalse(onErrorCalled)
logEntriesResult = GeckoResult()
whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
logEntriesResult.completeExceptionally(Exception())
engine.getTrackersLog(
mockSession,
onSuccess = {
trackersLog = it
onSuccessCalled = true
},
onError = { onErrorCalled = true }
)
assertTrue(onErrorCalled)
}
@Test
fun `install web extension successfully but do not allow content messaging`() {
val runtime = mock<GeckoRuntime>()
......@@ -525,25 +468,27 @@ class GeckoEngineTest {
}
@Test
fun `register web extension tab delegate`() {
fun `register web extension delegate`() {
val runtime: GeckoRuntime = mock()
val webExtensionController: WebExtensionController = mock()
whenever(runtime.webExtensionController).thenReturn(webExtensionController)
val webExtensionsTabDelegate: WebExtensionTabDelegate = mock()
val webExtensionsDelegate: WebExtensionDelegate = mock()
val engine = GeckoEngine(context, runtime = runtime)
engine.registerWebExtensionTabDelegate(webExtensionsTabDelegate)
engine.registerWebExtensionDelegate(webExtensionsDelegate)
// Verify we set the delegate and notify onNewTab
val captor = argumentCaptor<WebExtensionController.TabDelegate>()
verify(webExtensionController).tabDelegate = captor.capture()
val engineSessionCaptor = argumentCaptor<GeckoEngineSession>()
captor.value.onNewTab(null, null)
verify(webExtensionsTabDelegate).onNewTab(eq(null), eq(""), engineSessionCaptor.capture())
verify(webExtensionsDelegate).onNewTab(eq(null), eq(""), engineSessionCaptor.capture())
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
captor.value.onNewTab(null, "https://www.mozilla.org")
verify(webExtensionsTabDelegate).onNewTab(eq(null), eq("https://www.mozilla.org"),
verify(webExtensionsDelegate).onNewTab(eq(null), eq("https://www.mozilla.org"),
engineSessionCaptor.capture())
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
......@@ -551,12 +496,23 @@ class GeckoEngineTest {
val webExt = org.mozilla.geckoview.WebExtension("test")
val geckoExtCap = argumentCaptor<mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension>()
captor.value.onNewTab(webExt, "https://test-moz.org")
verify(webExtensionsTabDelegate).onNewTab(geckoExtCap.capture(), eq("https://test-moz.org"),
verify(webExtensionsDelegate).onNewTab(geckoExtCap.capture(), eq("https://test-moz.org"),
engineSessionCaptor.capture())
assertNotNull(geckoExtCap.value)
assertEquals(geckoExtCap.value.id, webExt.id)
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
// Verify we notify onInstalled
val result = GeckoResult<Void>()
whenever(runtime.registerWebExtension(any())).thenReturn(result)
engine.installWebExtension("test-webext", "resource://android/assets/extensions/test")
result.complete(null)
val extCaptor = argumentCaptor<WebExtension>()
verify(webExtensionsDelegate).onInstalled(extCaptor.capture())
assertEquals("test-webext", extCaptor.value.id)
assertEquals("resource://android/assets/extensions/test", extCaptor.value.url)
}
@Test(expected = RuntimeException::class)
......@@ -680,6 +636,64 @@ class GeckoEngineTest {
verify(mockStore).restore()
}
@Test
fun `fetch trackers logged successfully`() {
val runtime = mock<GeckoRuntime>()
val engine = GeckoEngine(context, runtime = runtime)
var onSuccessCalled = false
var onErrorCalled = false
val mockSession = mock<GeckoEngineSession>()
var trackersLog: List<TrackerLog>? = null
val mockContentBlockingController = mock<ContentBlockingController>()
var logEntriesResult = GeckoResult<List<ContentBlockingController.LogEntry>>()
whenever(runtime.contentBlockingController).thenReturn(mockContentBlockingController)
whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
engine.getTrackersLog(
mockSession,
onSuccess = {
trackersLog = it
onSuccessCalled = true
},
onError = { onErrorCalled = true }
)
logEntriesResult.complete(createDummyLogEntryList())
val trackerLog = trackersLog!!.first()
assertTrue(trackerLog.cookiesHasBeenBlocked)
assertEquals("www.tracker.com", trackerLog.url)
assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES))
assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.FINGERPRINTING))
assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.CRYPTOMINING))
assertTrue(trackerLog.blockedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.SCRIPTS_AND_SUB_RESOURCES))
assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.FINGERPRINTING))
assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.CRYPTOMINING))
assertTrue(trackerLog.loadedCategories.contains(TrackingCategory.MOZILLA_SOCIAL))
assertTrue(onSuccessCalled)
assertFalse(onErrorCalled)
logEntriesResult = GeckoResult()
whenever(mockContentBlockingController.getLog(any())).thenReturn(logEntriesResult)
logEntriesResult.completeExceptionally(Exception())
engine.getTrackersLog(
mockSession,
onSuccess = {
trackersLog = it
onSuccessCalled = true
},
onError = { onErrorCalled = true }
)
assertTrue(onErrorCalled)
}
private fun createDummyLogEntryList(): List<ContentBlockingController.LogEntry> {
val addLogEntry = object : ContentBlockingController.LogEntry() {}
......
......@@ -21,6 +21,7 @@ import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.utils.EngineVersion
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import org.json.JSONObject
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoRuntime
......@@ -39,8 +40,8 @@ class GeckoEngine(
executorProvider: () -> GeckoWebExecutor = { GeckoWebExecutor(runtime) }
) : Engine {
private val executor by lazy { executorProvider.invoke() }
private val localeUpdater = LocaleSettingUpdater(context, runtime)
private var webExtensionDelegate: WebExtensionDelegate? = null
init {
runtime.delegate = GeckoRuntime.Delegate {
......@@ -97,6 +98,7 @@ class GeckoEngine(
) {
GeckoWebExtension(id, url, allowContentMessaging).also { ext ->
runtime.registerWebExtension(ext.nativeExtension).then({
webExtensionDelegate?.onInstalled(ext)
onSuccess(ext)
GeckoResult<Void>()
}, {
......@@ -106,6 +108,15 @@ class GeckoEngine(
}
}
/**
* See [Engine.registerWebExtensionDelegate].
*/
override fun registerWebExtensionDelegate(
webExtensionDelegate: WebExtensionDelegate
) {
this.webExtensionDelegate = webExtensionDelegate
}
/**
* See [Engine.clearData].
*/
......
......@@ -15,6 +15,8 @@ import mozilla.components.concept.engine.EngineSession.SafeBrowsingPolicy
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy
import mozilla.components.concept.engine.UnsupportedSettingException
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
......@@ -355,6 +357,25 @@ class GeckoEngineTest {
assertEquals(expected, throwable)
}
@Test
fun `register web extension delegate`() {
val runtime: GeckoRuntime = mock()
val webExtensionsDelegate: WebExtensionDelegate = mock()
val engine = GeckoEngine(context, runtime = runtime)
engine.registerWebExtensionDelegate(webExtensionsDelegate)
// Verify we notify onInstalled
val result = GeckoResult<Void>()
whenever(runtime.registerWebExtension(any())).thenReturn(result)
engine.installWebExtension("test-webext", "resource://android/assets/extensions/test")
result.complete(null)
val extCaptor = argumentCaptor<WebExtension>()
verify(webExtensionsDelegate).onInstalled(extCaptor.capture())
assertEquals("test-webext", extCaptor.value.id)
assertEquals("resource://android/assets/extensions/test", extCaptor.value.url)
}
@Test(expected = RuntimeException::class)
fun `WHEN GeckoRuntime is shutting down THEN GeckoEngine throws runtime exception`() {
val runtime: GeckoRuntime = mock()
......
......@@ -11,7 +11,7 @@ import mozilla.components.concept.engine.content.blocking.TrackingProtectionExce
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.utils.EngineVersion
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import org.json.JSONObject
import java.lang.UnsupportedOperationException
......@@ -131,12 +131,13 @@ interface Engine {
): Unit = onError(id, UnsupportedOperationException("Web extension support is not available in this engine"))
/**
* Registers a [WebExtensionTabDelegate] to be notified when web extensions attempt to open/close tabs.
* Registers a [WebExtensionDelegate] to be notified of engine events
* related to web extensions
*
* @param webExtensionTabDelegate callback is invoked when a web extension opens a new tab.
* @param webExtensionDelegate callback to be invoked for web extension events.
*/
fun registerWebExtensionTabDelegate(
webExtensionTabDelegate: WebExtensionTabDelegate
fun registerWebExtensionDelegate(
webExtensionDelegate: WebExtensionDelegate
): Unit = throw UnsupportedOperationException("Web extension support is not available in this engine")
/**
......
......@@ -7,16 +7,27 @@ package mozilla.components.concept.engine.webextension
import mozilla.components.concept.engine.EngineSession
/**
* Notifies applications / other components that a web extension wants to open a new tab via
* browser.tabs.create. Note that browser.tabs.remove is currently not supported:
* https://github.com/mozilla-mobile/android-components/issues/4682
* Notifies applications or other components of engine events related to web
* extensions e.g. an extension was installed, or an extension wants to open
* a new tab.
*/
interface WebExtensionTabDelegate {
interface WebExtensionDelegate {
/**
* Invoked when a web extension opens a new tab.
* Invoked when a web extension was installed successfully.
*
* @param webExtension An instance of [WebExtension] or null if extension was not registered with the engine.
* @param webExtension The installed extension.
*/
fun onInstalled(webExtension: WebExtension) = Unit
/**
* Invoked when a web extension attempts to open a new tab via
* browser.tabs.create. Note that browser.tabs.remove is currently
* not supported:
* https://github.com/mozilla-mobile/android-components/issues/4682
*
* @param webExtension An instance of [WebExtension] or null if extension
* was not registered with the engine.
* @param url the target url to be loaded in a new tab.
* @param engineSession an instance of engine session to open a new tab with.
*/
......
......@@ -29,6 +29,7 @@ android {
dependencies {
implementation project(':concept-engine')
implementation project(':browser-state')
implementation project(':support-base')
implementation Dependencies.kotlin_stdlib
......
/* 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 mozilla.components.support.webextensions
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
/**
* Provides functionality to make sure web extension related events in the
* [Engine] are reflected in the browser state by dispatching the
* corresponding actions to the [BrowserStore].
*/
object WebExtensionSupport {
/**
* Registers a listener for web extension related events on the provided
* [Engine] and reacts by dispatching the corresponding actions to the
* provided [BrowserStore].
*
* @param engine the browser [Engine] to use.
* @param store the application's [BrowserStore].
* @param onNewTabOverride (optional) override of behaviour that should
* be triggered when web extensions open a new tab e.g. when dispatching
* to the store isn't sufficient while migrating from browser-session
* to browser-state.
*/
fun initialize(
engine: Engine,
store: BrowserStore,
onNewTabOverride: ((WebExtension?, EngineSession, String) -> Unit)? = null
) {
engine.registerWebExtensionDelegate(object : WebExtensionDelegate {
override fun onNewTab(webExtension: WebExtension?, url: String, engineSession: EngineSession) {
onNewTabOverride?.invoke(webExtension, engineSession, url)
?: store.dispatch(TabListAction.AddTabAction(createTab(url)))
}