diff --git a/browser/components/newtab/lib/ASRouterTriggerListeners.jsm b/browser/components/newtab/lib/ASRouterTriggerListeners.jsm index d663c29dd1b9..ae9fd0d6a014 100644 --- a/browser/components/newtab/lib/ASRouterTriggerListeners.jsm +++ b/browser/components/newtab/lib/ASRouterTriggerListeners.jsm @@ -323,6 +323,7 @@ this.ASRouterTriggerListeners = new Map([ _triggerHandler: null, _hosts: null, _matchPatternSet: null, + _visits: null, /* * If the listener is already initialised, `init` will replace the trigger @@ -347,6 +348,7 @@ this.ASRouterTriggerListeners = new Map([ } ); + this._visits = new Map(); this._initialized = true; } this._triggerHandler = triggerHandler; @@ -371,6 +373,7 @@ this.ASRouterTriggerListeners = new Map([ this._triggerHandler = null; this._hosts = null; this._matchPatternSet = null; + this._visits = null; } }, @@ -388,7 +391,13 @@ this.ASRouterTriggerListeners = new Map([ aRequest ); if (match) { - this._triggerHandler(aBrowser, { id: this.id, param: match }); + let visitsCount = (this._visits.get(match.url) || 0) + 1; + this._visits.set(match.url, visitsCount); + this._triggerHandler(aBrowser, { + id: this.id, + param: match, + context: { visitsCount }, + }); } } }, diff --git a/browser/components/newtab/test/browser/browser.ini b/browser/components/newtab/test/browser/browser.ini index 72ff8fc99f5f..7875906ce630 100644 --- a/browser/components/newtab/test/browser/browser.ini +++ b/browser/components/newtab/test/browser/browser.ini @@ -44,3 +44,4 @@ tags = remote-settings [browser_asrouter_experimentsAPILoader.js] [browser_asrouter_group_frequency.js] [browser_asrouter_group_userprefs.js] +[browser_trigger_listeners.js] diff --git a/browser/components/newtab/test/browser/browser_asrouter_targeting.js b/browser/components/newtab/test/browser/browser_asrouter_targeting.js index 94b1c0eca478..13055792afa8 100644 --- a/browser/components/newtab/test/browser/browser_asrouter_targeting.js +++ b/browser/components/newtab/test/browser/browser_asrouter_targeting.js @@ -1110,3 +1110,26 @@ add_task(async function check_newTabSettings_webExtension() { AboutNewTab.resetNewTabURL(); }); + +add_task(async function check_openUrlTrigger_context() { + const message = { + ...CFRMessageProvider.getMessages().find(m => m.id === "YOUTUBE_ENHANCE_3"), + targeting: "visitsCount == 3", + }; + const trigger = { + id: "openURL", + context: { visitsCount: 3 }, + param: { host: "youtube.com", url: "https://www.youtube.com" }, + }; + + is( + ( + await ASRouterTargeting.findMatchingMessage({ + messages: [message], + trigger, + }) + ).id, + message.id, + `should select ${message.id} mesage` + ); +}); diff --git a/browser/components/newtab/test/browser/browser_trigger_listeners.js b/browser/components/newtab/test/browser/browser_trigger_listeners.js new file mode 100644 index 000000000000..c10eafa326ed --- /dev/null +++ b/browser/components/newtab/test/browser/browser_trigger_listeners.js @@ -0,0 +1,65 @@ +const { ASRouterTriggerListeners } = ChromeUtils.import( + "resource://activity-stream/lib/ASRouterTriggerListeners.jsm" +); + +add_task(async function setup() { + registerCleanupFunction(() => { + const trigger = ASRouterTriggerListeners.get("openURL"); + trigger.uninit(); + }); +}); + +add_task(async function test_openURL_visit_counter() { + const trigger = ASRouterTriggerListeners.get("openURL"); + const stub = sinon.stub(); + trigger.uninit(); + + trigger.init(stub, ["example.com"]); + + await waitForUrlLoad("about:blank"); + await waitForUrlLoad("https://example.com/"); + await waitForUrlLoad("about:blank"); + await waitForUrlLoad("http://example.com/"); + await waitForUrlLoad("about:blank"); + await waitForUrlLoad("http://example.com/"); + + Assert.equal(stub.callCount, 3, "Stub called 3 times for example.com host"); + Assert.equal( + stub.firstCall.args[1].context.visitsCount, + 1, + "First call should have count 1" + ); + Assert.equal( + stub.thirdCall.args[1].context.visitsCount, + 2, + "Third call should have count 2 for http://example.com" + ); +}); + +add_task(async function test_openURL_visit_counter_withPattern() { + const trigger = ASRouterTriggerListeners.get("openURL"); + const stub = sinon.stub(); + trigger.uninit(); + + // Match any valid URL + trigger.init(stub, [], ["*://*/*"]); + + await waitForUrlLoad("about:blank"); + await waitForUrlLoad("https://example.com/"); + await waitForUrlLoad("about:blank"); + await waitForUrlLoad("http://example.com/"); + await waitForUrlLoad("about:blank"); + await waitForUrlLoad("http://example.com/"); + + Assert.equal(stub.callCount, 3, "Stub called 3 times for example.com host"); + Assert.equal( + stub.firstCall.args[1].context.visitsCount, + 1, + "First call should have count 1" + ); + Assert.equal( + stub.thirdCall.args[1].context.visitsCount, + 2, + "Third call should have count 2 for http://example.com" + ); +}); diff --git a/browser/components/newtab/test/browser/head.js b/browser/components/newtab/test/browser/head.js index 5a4ca1f9c6d9..c37c701d55c4 100644 --- a/browser/components/newtab/test/browser/head.js +++ b/browser/components/newtab/test/browser/head.js @@ -72,6 +72,17 @@ async function waitForPreloaded(browser) { } } +/** + * Helper function to navigate and wait for page to load + * https://searchfox.org/mozilla-central/rev/b2716c233e9b4398fc5923cbe150e7f83c7c6c5b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm#383 + */ +// eslint-disable-next-line no-unused-vars +async function waitForUrlLoad(url) { + let browser = gBrowser.selectedBrowser; + await BrowserTestUtils.loadURI(browser, url); + await BrowserTestUtils.browserLoaded(browser, false, url); +} + /** * Helper to force the HighlightsFeed to update. */ diff --git a/browser/components/newtab/test/unit/asrouter/ASRouterTriggerListeners.test.js b/browser/components/newtab/test/unit/asrouter/ASRouterTriggerListeners.test.js index ce36c2fccd27..ed7137f1dab5 100644 --- a/browser/components/newtab/test/unit/asrouter/ASRouterTriggerListeners.test.js +++ b/browser/components/newtab/test/unit/asrouter/ASRouterTriggerListeners.test.js @@ -365,6 +365,7 @@ describe("ASRouterTriggerListeners", () => { assert.calledWithExactly(newTriggerHandler, browser, { id: "openURL", param: { host: "www.mozilla.org", url: "www.mozilla.org" }, + context: { visitsCount: 1 }, }); }); it("should call triggerHandler for a redirect (openURL + frequentVisits)", () => { @@ -417,6 +418,7 @@ describe("ASRouterTriggerListeners", () => { assert.calledWithExactly(newTriggerHandler, browser, { id: "openURL", param: { host: "www.mozilla.org", url: "www.mozilla.org" }, + context: { visitsCount: 1 }, }); }); it("should call triggerHandler for a redirect (openURL + frequentVisits)", () => { @@ -469,6 +471,7 @@ describe("ASRouterTriggerListeners", () => { assert.calledWithExactly(newTriggerHandler, browser, { id: "openURL", param: { host: "www.mozilla.org", url: "www.mozilla.org" }, + context: { visitsCount: 1 }, }); }); it("should fail for subdomains (not redirect)", () => { diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md index a70551b98662..5dd68bea3e2c 100644 --- a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md +++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md @@ -75,6 +75,12 @@ let recentVisits: visit[]; ### `openURL` Happens every time the user loads a new URL that matches the provided `hosts` or `patterns`. +During a browsing session it keeps track of visits to unique urls that can be used inside targeting expression. + +```javascript +// True on the third visit for the URL which the trigger matched on +visitsCount >= 3 +``` ### `newSavedLogin`