summaryrefslogtreecommitdiffstats
path: root/browser/components/places/tests/browser/interactions
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/places/tests/browser/interactions')
-rw-r--r--browser/components/places/tests/browser/interactions/browser.ini30
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js108
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_referrer.js45
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js162
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js208
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_typing.js410
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js171
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_view_time.js398
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js123
-rw-r--r--browser/components/places/tests/browser/interactions/head.js200
-rw-r--r--browser/components/places/tests/browser/interactions/scrolling.html121
-rw-r--r--browser/components/places/tests/browser/interactions/scrolling_subframe.html25
12 files changed, 2001 insertions, 0 deletions
diff --git a/browser/components/places/tests/browser/interactions/browser.ini b/browser/components/places/tests/browser/interactions/browser.ini
new file mode 100644
index 0000000000..79be009cbf
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser.ini
@@ -0,0 +1,30 @@
+# 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/.
+
+[DEFAULT]
+prefs =
+ browser.places.interactions.enabled=true
+ browser.places.interactions.log=true
+ browser.places.interactions.scrolling_timeout_ms=50
+ general.smoothScroll=false
+
+support-files =
+ head.js
+ ../keyword_form.html
+ scrolling.html
+ scrolling_subframe.html
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+
+[browser_interactions_blocklist.js]
+[browser_interactions_referrer.js]
+[browser_interactions_scrolling.js]
+skip-if =
+ apple_silicon && fission # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+[browser_interactions_scrolling_dom_history.js]
+skip-if = os == 'mac' # Bug 1756157: Randomly times out on macOS
+[browser_interactions_typing.js]
+[browser_interactions_typing_dom_history.js]
+[browser_interactions_view_time.js]
+[browser_interactions_view_time_dom_history.js]
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js b/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js
new file mode 100644
index 0000000000..c174024ef5
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that interactions are not recorded for sites on the blocklist.
+ */
+
+const ALLOWED_TEST_URL = "http://mochi.test:8888/";
+const BLOCKED_TEST_URL = "https://example.com/browser";
+
+ChromeUtils.defineESModuleGetters(this, {
+ InteractionsBlocklist: "resource:///modules/InteractionsBlocklist.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FilterAdult: "resource://activity-stream/lib/FilterAdult.jsm",
+});
+
+add_setup(async function () {
+ let oldBlocklistValue = Services.prefs.getStringPref(
+ "places.interactions.customBlocklist",
+ "[]"
+ );
+ Services.prefs.setStringPref("places.interactions.customBlocklist", "[]");
+
+ registerCleanupFunction(async () => {
+ Services.prefs.setStringPref(
+ "places.interactions.customBlocklist",
+ oldBlocklistValue
+ );
+ });
+});
+/**
+ * Loads the blocked URL, then loads about:blank to trigger the end of the
+ * interaction with the blocked URL.
+ *
+ * @param {boolean} expectRecording
+ * True if we expect the blocked URL to have been recorded in the database.
+ */
+async function loadBlockedUrl(expectRecording) {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(ALLOWED_TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.loadURIString(browser, BLOCKED_TEST_URL);
+ await BrowserTestUtils.browserLoaded(browser, false, BLOCKED_TEST_URL);
+
+ await assertDatabaseValues([
+ {
+ url: ALLOWED_TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ if (expectRecording) {
+ await assertDatabaseValues([
+ {
+ url: ALLOWED_TEST_URL,
+ totalViewTime: 10000,
+ },
+ {
+ url: BLOCKED_TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+ } else {
+ await assertDatabaseValues([
+ {
+ url: ALLOWED_TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ }
+ });
+}
+
+add_task(async function test_regexp() {
+ info("Record BLOCKED_TEST_URL because it is not yet blocklisted.");
+ await loadBlockedUrl(true);
+
+ info("Add BLOCKED_TEST_URL to the blocklist and verify it is not recorded.");
+ let blockedRegex = /^(https?:\/\/)?example\.com\/browser/i;
+ InteractionsBlocklist.addRegexToBlocklist(blockedRegex);
+ Assert.equal(
+ Services.prefs.getStringPref("places.interactions.customBlocklist", "[]"),
+ JSON.stringify([blockedRegex.toString()])
+ );
+ await loadBlockedUrl(false);
+
+ info("Remove BLOCKED_TEST_URL from the blocklist and verify it is recorded.");
+ InteractionsBlocklist.removeRegexFromBlocklist(blockedRegex);
+ Assert.equal(
+ Services.prefs.getStringPref("places.interactions.customBlocklist", "[]"),
+ JSON.stringify([])
+ );
+ await loadBlockedUrl(true);
+});
+
+add_task(async function test_adult() {
+ FilterAdult.addDomainToList("https://example.com/browser");
+ await loadBlockedUrl(false);
+ FilterAdult.removeDomainFromList("https://example.com/browser");
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js b/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js
new file mode 100644
index 0000000000..fda93bfc5a
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js
@@ -0,0 +1,45 @@
+/* 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/. */
+
+/**
+ * Tests page view time recording for interactions.
+ */
+
+const TEST_REFERRER_URL = "https://example.org/browser";
+const TEST_URL = "https://example.org/browser/browser";
+
+add_task(async function test_interactions_referrer() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_REFERRER_URL, async browser => {
+ let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ );
+
+ // Load a new URI with a specific referrer.
+ let referrerInfo1 = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ Services.io.newURI(TEST_REFERRER_URL)
+ );
+ browser.loadURI(Services.io.newURI(TEST_URL), {
+ referrerInfo: referrerInfo1,
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ });
+ await BrowserTestUtils.browserLoaded(browser, true, TEST_URL);
+ });
+ await assertDatabaseValues([
+ {
+ url: TEST_REFERRER_URL,
+ referrer_url: null,
+ },
+ {
+ url: TEST_URL,
+ referrer_url: TEST_REFERRER_URL,
+ },
+ ]);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js
new file mode 100644
index 0000000000..925d427ff5
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js
@@ -0,0 +1,162 @@
+/* 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/. */
+
+/**
+ * Test reporting of scrolling interactions.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/interactions/scrolling.html";
+const TEST_URL2 = "https://example.com/browser";
+
+async function waitForScrollEvent(aBrowser, aTask) {
+ let promise = BrowserTestUtils.waitForContentEvent(aBrowser, "scroll");
+
+ // This forces us to send a message to the browser's process and receive a response which ensures
+ // that the message sent to register the scroll event listener will also have been processed by
+ // the content process. Without this it is possible for our scroll task to send a higher priority
+ // message which can be processed by the content process before the message to register the scroll
+ // event listener.
+ await SpecialPowers.spawn(aBrowser, [], () => {});
+
+ await aTask();
+ await promise;
+}
+
+add_task(async function test_no_scrolling() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_arrow_key_down_scroll() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scrollIntoView() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await waitForScrollEvent(browser, () =>
+ SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("middleHeading");
+ heading.scrollIntoView();
+ })
+ );
+
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ // JS-triggered scrolling should not be reported
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_anchor_click() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await waitForScrollEvent(browser, () =>
+ SpecialPowers.spawn(browser, [], function () {
+ const anchor = content.document.getElementById("to_bottom_anchor");
+ anchor.click();
+ })
+ );
+
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ // The scrolling resulting from clicking on an anchor should not be reported
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_window_scrollBy() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await waitForScrollEvent(browser, () =>
+ SpecialPowers.spawn(browser, [], function () {
+ content.scrollBy(0, 100);
+ })
+ );
+
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ // The scrolling resulting from the window.scrollBy() call should not be reported
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_window_scrollTo() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await waitForScrollEvent(browser, () =>
+ SpecialPowers.spawn(browser, [], function () {
+ content.scrollTo(0, 200);
+ })
+ );
+
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ // The scrolling resulting from the window.scrollTo() call should not be reported
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js
new file mode 100644
index 0000000000..e272f6c866
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js
@@ -0,0 +1,208 @@
+/* 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/. */
+
+/**
+ * Test reporting of scrolling interactions after DOM history API use.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/interactions/scrolling.html";
+const TEST_URL2 = "https://example.com/browser";
+
+async function waitForScrollEvent(aBrowser, aTask) {
+ let promise = BrowserTestUtils.waitForContentEvent(aBrowser, "scroll");
+
+ // This forces us to send a message to the browser's process and receive a response which ensures
+ // that the message sent to register the scroll event listener will also have been processed by
+ // the content process. Without this it is possible for our scroll task to send a higher priority
+ // message which can be processed by the content process before the message to register the scroll
+ // event listener.
+ await SpecialPowers.spawn(aBrowser, [], () => {});
+
+ await aTask();
+ await promise;
+}
+
+add_task(async function test_scroll_pushState() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scroll_pushState_sameUrl() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ // As the page hasn't changed there will be no interactions saved yet.
+ await assertDatabaseValues([]);
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scroll_replaceState() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scroll_replaceState_sameUrl() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ // As the page hasn't changed there will be no interactions saved yet.
+ await assertDatabaseValues([]);
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scroll_hashchange() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL + "#foo", url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_typing.js b/browser/components/places/tests/browser/interactions/browser_interactions_typing.js
new file mode 100644
index 0000000000..6561a4d334
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_typing.js
@@ -0,0 +1,410 @@
+/* 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/. */
+
+/**
+ * Tests reporting of typing interactions.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html";
+const TEST_URL2 = "https://example.com/browser";
+const TEST_URL3 =
+ "https://example.com/browser/browser/base/content/test/contextMenu/subtst_contextmenu_input.html";
+
+const sentence = "The quick brown fox jumps over the lazy dog.";
+const sentenceFragments = [
+ "The quick",
+ " brown fox",
+ " jumps over the lazy dog.",
+];
+const longSentence =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut purus a libero cursus scelerisque. In hac habitasse platea dictumst. Quisque posuere ante sed consequat volutpat.";
+
+// For tests where it matters reduces the maximum time between keypresses to a length that we can
+// afford to delay the test by.
+const PREF_TYPING_DELAY = "browser.places.interactions.typing_timeout_ms";
+const POST_TYPING_DELAY = 150;
+
+async function sendTextToInput(browser, text) {
+ await SpecialPowers.spawn(browser, [], function () {
+ const input = content.document.querySelector(
+ "#form1 > input[name='search']"
+ );
+ input.focus();
+ input.value = ""; // Reset to later verify that the provided text matches the value
+ });
+
+ EventUtils.sendString(text);
+
+ await SpecialPowers.spawn(browser, [{ text }], async function (args) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector("#form1 > input[name='search']").value ==
+ args.text,
+ "Text has been set on input"
+ );
+ });
+}
+
+add_task(async function test_load_and_navigate_away_no_keypresses() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ {
+ url: TEST_URL2,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_load_type_and_navigate_away() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_no_typing_close_tab() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {});
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+});
+
+add_task(async function test_typing_close_tab() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+});
+
+add_task(async function test_single_key_typing_and_delay() {
+ await Interactions.reset();
+
+ Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY);
+
+ try {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ // Single keystrokes with a delay between each, are not considered typing
+ const text = ["T", "h", "e"];
+
+ for (let i = 0; i < text.length; i++) {
+ await sendTextToInput(browser, text[i]);
+
+ // We do need to wait here because typing is defined as a series of keystrokes followed by a delay.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2));
+ }
+ });
+ } finally {
+ Services.prefs.clearUserPref(PREF_TYPING_DELAY);
+ }
+
+ // Since we typed single keys with delays between each, there should be no typing added to the database
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+});
+
+add_task(async function test_double_key_typing_and_delay() {
+ await Interactions.reset();
+
+ // Test three 2-key typing bursts.
+ const text = ["Ab", "cd", "ef"];
+
+ const testStartTime = Cu.now();
+
+ Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY);
+
+ try {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ for (let i = 0; i < text.length; i++) {
+ await sendTextToInput(browser, text[i]);
+
+ // We do need to wait here because typing is defined as a series of keystrokes followed by a delay.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2));
+ }
+ });
+ } finally {
+ Services.prefs.clearUserPref(PREF_TYPING_DELAY);
+ }
+
+ // All keys should be recorded
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: text.reduce(
+ (accumulator, current) => accumulator + current.length,
+ 0
+ ),
+ typingTimeIsLessThan: Cu.now() - testStartTime,
+ },
+ ]);
+});
+
+add_task(async function test_typing_and_delay() {
+ await Interactions.reset();
+
+ const testStartTime = Cu.now();
+
+ Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY);
+
+ try {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ for (let i = 0; i < sentenceFragments.length; i++) {
+ await sendTextToInput(browser, sentenceFragments[i]);
+
+ // We do need to wait here because typing is defined as a series of keystrokes followed by a delay.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2));
+ }
+ });
+ } finally {
+ Services.prefs.clearUserPref(PREF_TYPING_DELAY);
+ }
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentenceFragments.reduce(
+ (accumulator, current) => accumulator + current.length,
+ 0
+ ),
+ typingTimeIsGreaterThan: 0,
+ typingTimeIsLessThan: Cu.now() - testStartTime,
+ },
+ ]);
+});
+
+add_task(async function test_typing_and_reload() {
+ await Interactions.reset();
+
+ const testStartTime = Cu.now();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentenceFragments[0]);
+
+ info("reload");
+ browser.reload();
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
+
+ // First typing should have been recorded
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentenceFragments[0].length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ await sendTextToInput(browser, sentenceFragments[1]);
+
+ info("reload");
+ browser.reload();
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
+
+ // Second typing should have been recorded
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentenceFragments[0].length,
+ typingTimeIsGreaterThan: 0,
+ typingTimeIsLessThan: Cu.now() - testStartTime,
+ },
+ {
+ url: TEST_URL,
+ keypresses: sentenceFragments[1].length,
+ typingTimeIsGreaterThan: 0,
+ typingTimeIsLessThan: Cu.now() - testStartTime,
+ },
+ ]);
+ });
+}).skip(); // Bug 1749328 - intermittent failure: dropping the 2nd interaction after the 2nd reload
+
+add_task(async function test_switch_tabs_no_typing() {
+ await Interactions.reset();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2);
+
+ info("Switch to second tab");
+ gBrowser.selectedTab = tab2;
+
+ // Only the interaction of the first tab should be recorded so far, and with no typing
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+add_task(async function test_typing_switch_tabs() {
+ await Interactions.reset();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ await sendTextToInput(tab1.linkedBrowser, sentence);
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL3);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL3);
+
+ info("Switch to second tab");
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+
+ // Only the interaction of the first tab should be recorded so far
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ const tab1TyingTime = await getDatabaseValue(TEST_URL, "typingTime");
+
+ info("Switch back to first tab");
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ // The interaction of the second tab should now be recorded (no typing)
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ exactTypingTime: tab1TyingTime,
+ },
+ {
+ url: TEST_URL3,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+
+ info("Switch back to the second tab");
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ exactTypingTime: tab1TyingTime,
+ },
+ {
+ url: TEST_URL3,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+
+ // Typing into the second tab
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], function () {
+ const input = content.document.getElementById("input_text");
+ input.focus();
+ });
+ await EventUtils.sendString(longSentence);
+ await TestUtils.waitForTick();
+
+ info("Switch back to first tab");
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ // The interaction of the second tab should now also be recorded (with typing)
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ exactTypingTime: tab1TyingTime,
+ },
+ {
+ url: TEST_URL3,
+ keypresses: longSentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js
new file mode 100644
index 0000000000..7462a5080a
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js
@@ -0,0 +1,171 @@
+/* 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/. */
+
+/**
+ * Tests reporting of typing interactions after DOM history API usage.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html";
+const TEST_URL2 = "https://example.com/browser";
+
+const sentence = "The quick brown fox jumps over the lazy dog.";
+
+async function sendTextToInput(browser, text) {
+ await SpecialPowers.spawn(browser, [], function () {
+ const input = content.document.querySelector(
+ "#form1 > input[name='search']"
+ );
+ input.focus();
+ input.value = ""; // Reset to later verify that the provided text matches the value
+ });
+
+ EventUtils.sendString(text);
+
+ await SpecialPowers.spawn(browser, [{ text }], async function (args) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector("#form1 > input[name='search']").value ==
+ args.text,
+ "Text has been set on input"
+ );
+ });
+}
+
+add_task(async function test_typing_pushState() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_typing_pushState_sameUrl() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length * 2,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_typing_replaceState() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_typing_replaceState_sameUrl() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length * 2,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_typing_hashchange() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL + "#foo", url => {
+ content.location = url;
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length * 2,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js b/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js
new file mode 100644
index 0000000000..3bb76288eb
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js
@@ -0,0 +1,398 @@
+/* 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/. */
+
+/**
+ * Tests page view time recording for interactions.
+ */
+
+const TEST_URL = "https://example.com/";
+const TEST_URL2 = "https://example.com/browser";
+const TEST_URL3 = "https://example.com/browser/browser";
+const TEST_URL4 = "https://example.com/browser/browser/components";
+
+add_task(async function test_interactions_simple_load_and_navigate_away() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.loadURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+
+ BrowserTestUtils.loadURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_interactions_simple_load_and_change_to_non_http() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.loadURIString(browser, "about:support");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:support");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_interactions_close_tab() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_background_tab() {
+ await Interactions.reset();
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2);
+
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.removeTab(tab2);
+
+ // This is checking a non-action, so let the event queue clear to try and
+ // detect any unexpected database writes. We wait for a few ticks to
+ // make it more likely. however if this fails it may show up as an
+ // intermittent.
+ await TestUtils.waitForTick();
+ await TestUtils.waitForTick();
+ await TestUtils.waitForTick();
+
+ await assertDatabaseValues([]);
+
+ BrowserTestUtils.removeTab(tab1);
+
+ // Only the interaction in the visible tab should have been recorded.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_switch_tabs() {
+ await Interactions.reset();
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2);
+
+ info("Switch to second tab");
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+ gBrowser.selectedTab = tab2;
+
+ // Only the interaction of the first tab should be recorded so far.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ let tab1ViewTime = await getDatabaseValue(TEST_URL, "totalViewTime");
+
+ info("Switch back to first tab");
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ gBrowser.selectedTab = tab1;
+
+ // The interaction of the second tab should now be recorded.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: tab1ViewTime,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+
+ info("Switch to second tab again");
+ Interactions._pageViewStartTime = Cu.now() - 30000;
+ gBrowser.selectedTab = tab2;
+
+ // The interaction of the second tab should now be recorded.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: tab1ViewTime + 30000,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+add_task(async function test_interactions_switch_windows() {
+ await Interactions.reset();
+
+ // Open a tab in the first window.
+ let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ // and then load the second window.
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ BrowserTestUtils.loadURIString(otherWin.gBrowser.selectedBrowser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(
+ otherWin.gBrowser.selectedBrowser,
+ false,
+ TEST_URL2
+ );
+ await SimpleTest.promiseFocus(otherWin);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ let originalWindowViewTime = await getDatabaseValue(
+ TEST_URL,
+ "totalViewTime"
+ );
+
+ info("Switch back to original window");
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ await SimpleTest.promiseFocus(window);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: originalWindowViewTime,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+ let newWindowViewTime = await getDatabaseValue(TEST_URL2, "totalViewTime");
+
+ info("Switch back to new window");
+ Interactions._pageViewStartTime = Cu.now() - 30000;
+ await SimpleTest.promiseFocus(otherWin);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: originalWindowViewTime + 30000,
+ },
+ {
+ url: TEST_URL2,
+ exactTotalViewTime: newWindowViewTime,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tabInOriginalWindow);
+ await BrowserTestUtils.closeWindow(otherWin);
+});
+
+add_task(async function test_interactions_loading_in_unfocused_windows() {
+ await Interactions.reset();
+
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ BrowserTestUtils.loadURIString(otherWin.gBrowser.selectedBrowser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(
+ otherWin.gBrowser.selectedBrowser,
+ false,
+ TEST_URL
+ );
+
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.loadURIString(otherWin.gBrowser.selectedBrowser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(
+ otherWin.gBrowser.selectedBrowser,
+ false,
+ TEST_URL2
+ );
+
+ // Only the interaction of the first tab should be recorded so far.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ let newWindowViewTime = await getDatabaseValue(TEST_URL, "totalViewTime");
+
+ // Open a tab in the background window, and then navigate somewhere else,
+ // this should not record an intereaction.
+ let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL3,
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+
+ BrowserTestUtils.loadURIString(tabInOriginalWindow.linkedBrowser, TEST_URL4);
+ await BrowserTestUtils.browserLoaded(
+ tabInOriginalWindow.linkedBrowser,
+ false,
+ TEST_URL4
+ );
+
+ // Only the interaction of the first tab should be recorded so far.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: newWindowViewTime,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tabInOriginalWindow);
+ await BrowserTestUtils.closeWindow(otherWin);
+});
+
+add_task(async function test_interactions_private_browsing() {
+ await Interactions.reset();
+
+ // Open a tab in the first window.
+ let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ // and then load the second window.
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ BrowserTestUtils.loadURIString(
+ privateWin.gBrowser.selectedBrowser,
+ TEST_URL2
+ );
+ await BrowserTestUtils.browserLoaded(
+ privateWin.gBrowser.selectedBrowser,
+ false,
+ TEST_URL2
+ );
+ await SimpleTest.promiseFocus(privateWin);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ let originalWindowViewTime = await getDatabaseValue(
+ TEST_URL,
+ "totalViewTime"
+ );
+
+ info("Switch back to original window");
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ // As we're checking for a non-action, wait for the focus to have definitely
+ // completed, and then let the event queues clear.
+ await SimpleTest.promiseFocus(window);
+ await TestUtils.waitForTick();
+
+ // The private window site should not be recorded.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: originalWindowViewTime,
+ },
+ ]);
+
+ info("Switch back to new window");
+ Interactions._pageViewStartTime = Cu.now() - 30000;
+ await SimpleTest.promiseFocus(privateWin);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: originalWindowViewTime + 30000,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tabInOriginalWindow);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function test_interactions_idle() {
+ await Interactions.reset();
+ let lastViewTime;
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ Interactions.observe(null, "idle", "");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ lastViewTime = await getDatabaseValue(TEST_URL, "totalViewTime");
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+
+ Interactions.observe(null, "active", "");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: lastViewTime,
+ },
+ ]);
+
+ Interactions._pageViewStartTime = Cu.now() - 30000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: lastViewTime + 30000,
+ maxViewTime: lastViewTime + 30000 + 10000,
+ },
+ ]);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js
new file mode 100644
index 0000000000..857a846417
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js
@@ -0,0 +1,123 @@
+/* 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/. */
+
+/**
+ * Tests page view time recording for interactions after DOM history API usage.
+ */
+
+const TEST_URL = "https://example.com/";
+const TEST_URL2 = "https://example.com/browser";
+
+add_task(async function test_interactions_pushState() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_pushState_sameUrl() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_replaceState() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_replaceState_sameUrl() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_hashchange() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL + "#foo", url => {
+ content.location = url;
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+});
diff --git a/browser/components/places/tests/browser/interactions/head.js b/browser/components/places/tests/browser/interactions/head.js
new file mode 100644
index 0000000000..91aff9d100
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/head.js
@@ -0,0 +1,200 @@
+/* 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/. */
+
+const { Interactions } = ChromeUtils.importESModule(
+ "resource:///modules/Interactions.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "pageViewIdleTime",
+ "browser.places.interactions.pageViewIdleTime",
+ 60
+);
+
+add_setup(async function global_setup() {
+ // Disable idle management because it interacts with our code, causing
+ // unexpected intermittent failures, we'll fake idle notifications when
+ // we need to test it.
+ let idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService(
+ Ci.nsIUserIdleService
+ );
+ idleService.removeIdleObserver(Interactions, pageViewIdleTime);
+ registerCleanupFunction(() => {
+ idleService.addIdleObserver(Interactions, pageViewIdleTime);
+ });
+
+ // Clean interactions for each test.
+ await Interactions.reset();
+ registerCleanupFunction(async () => {
+ await Interactions.reset();
+ });
+});
+
+/**
+ * Ensures that a list of interactions have been permanently stored.
+ *
+ * @param {Array} expected list of interactions to be found.
+ * @param {boolean} [dontFlush] Avoid flushing pending data.
+ */
+async function assertDatabaseValues(expected, { dontFlush = false } = {}) {
+ await Interactions.interactionUpdatePromise;
+ if (!dontFlush) {
+ await Interactions.store.flush();
+ }
+
+ let interactions = await PlacesUtils.withConnectionWrapper(
+ "head.js::assertDatabaseValues",
+ async db => {
+ let rows = await db.execute(`
+ SELECT h.url AS url, h2.url as referrer_url, total_view_time, key_presses, typing_time, scrolling_time, scrolling_distance
+ FROM moz_places_metadata m
+ JOIN moz_places h ON h.id = m.place_id
+ LEFT JOIN moz_places h2 ON h2.id = m.referrer_place_id
+ ORDER BY created_at ASC
+ `);
+ return rows.map(r => ({
+ url: r.getResultByName("url"),
+ referrerUrl: r.getResultByName("referrer_url"),
+ keypresses: r.getResultByName("key_presses"),
+ typingTime: r.getResultByName("typing_time"),
+ totalViewTime: r.getResultByName("total_view_time"),
+ scrollingTime: r.getResultByName("scrolling_time"),
+ scrollingDistance: r.getResultByName("scrolling_distance"),
+ }));
+ }
+ );
+ info(
+ `Found ${interactions.length} interactions:\n ${JSON.stringify(
+ interactions
+ )}`
+ );
+ Assert.equal(
+ interactions.length,
+ expected.length,
+ "Found the expected number of entries"
+ );
+ for (let i = 0; i < Math.min(expected.length, interactions.length); i++) {
+ let actual = interactions[i];
+ Assert.equal(
+ actual.url,
+ expected[i].url,
+ "Should have saved the page into the database"
+ );
+
+ if (expected[i].exactTotalViewTime != undefined) {
+ Assert.equal(
+ actual.totalViewTime,
+ expected[i].exactTotalViewTime,
+ "Should have kept the exact time."
+ );
+ } else if (expected[i].totalViewTime != undefined) {
+ Assert.greaterOrEqual(
+ actual.totalViewTime,
+ expected[i].totalViewTime,
+ "Should have stored the interaction time"
+ );
+ }
+
+ if (expected[i].maxViewTime != undefined) {
+ Assert.less(
+ actual.totalViewTime,
+ expected[i].maxViewTime,
+ "Should have recorded an interaction below the maximum expected"
+ );
+ }
+
+ if (expected[i].keypresses != undefined) {
+ Assert.equal(
+ actual.keypresses,
+ expected[i].keypresses,
+ "Should have saved the keypresses into the database"
+ );
+ }
+
+ if (expected[i].exactTypingTime != undefined) {
+ Assert.equal(
+ actual.typingTime,
+ expected[i].exactTypingTime,
+ "Should have stored the exact typing time."
+ );
+ } else if (expected[i].typingTimeIsGreaterThan != undefined) {
+ Assert.greater(
+ actual.typingTime,
+ expected[i].typingTimeIsGreaterThan,
+ "Should have stored at least this amount of typing time."
+ );
+ } else if (expected[i].typingTimeIsLessThan != undefined) {
+ Assert.less(
+ actual.typingTime,
+ expected[i].typingTimeIsLessThan,
+ "Should have stored less than this amount of typing time."
+ );
+ }
+
+ if (expected[i].exactScrollingDistance != undefined) {
+ Assert.equal(
+ actual.scrollingDistance,
+ expected[i].exactScrollingDistance,
+ "Should have scrolled by exactly least this distance"
+ );
+ } else if (expected[i].exactScrollingTime != undefined) {
+ Assert.greater(
+ actual.scrollingTime,
+ expected[i].exactScrollingTime,
+ "Should have scrolled for exactly least this duration"
+ );
+ }
+
+ if (expected[i].scrollingDistanceIsGreaterThan != undefined) {
+ Assert.greater(
+ actual.scrollingDistance,
+ expected[i].scrollingDistanceIsGreaterThan,
+ "Should have scrolled by at least this distance"
+ );
+ } else if (expected[i].scrollingTimeIsGreaterThan != undefined) {
+ Assert.greater(
+ actual.scrollingTime,
+ expected[i].scrollingTimeIsGreaterThan,
+ "Should have scrolled for at least this duration"
+ );
+ }
+ }
+}
+
+/**
+ * Ensures that a list of interactions have been permanently stored.
+ *
+ * @param {string} url The url to query.
+ * @param {string} property The property to extract.
+ */
+async function getDatabaseValue(url, property) {
+ await Interactions.store.flush();
+ const PROP_TRANSLATOR = {
+ totalViewTime: "total_view_time",
+ keypresses: "key_presses",
+ typingTime: "typing_time",
+ };
+ property = PROP_TRANSLATOR[property] || property;
+
+ return PlacesUtils.withConnectionWrapper(
+ "head.js::getDatabaseValue",
+ async db => {
+ let rows = await db.execute(
+ `
+ SELECT * FROM moz_places_metadata m
+ JOIN moz_places h ON h.id = m.place_id
+ WHERE url = :url
+ ORDER BY created_at DESC
+ `,
+ { url }
+ );
+ return rows?.[0].getResultByName(property);
+ }
+ );
+}
diff --git a/browser/components/places/tests/browser/interactions/scrolling.html b/browser/components/places/tests/browser/interactions/scrolling.html
new file mode 100644
index 0000000000..ce435097e6
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/scrolling.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=windows-1252">
+ <style>
+ .div {
+ height: 250px;
+ width: 250px;
+ overflow: auto;
+ }
+
+ .content {
+ height: 800px;
+ width: 2000px;
+ background-color: coral;
+ }
+ </style>
+</head>
+<body>
+
+ <h1 id="heading" >Scrolling interaction tests</h1>
+
+ <br><br><br>
+
+ <div class="div" id="scroll_div" >
+ <div class="content" id="scrollable_div_content" tabindex="-1">
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. <br> Maecenas vitae condimentum erat, vel luctus nulla. <br>Praesent nulla magna, rhoncus quis velit eu, dapibus efficitur lectus. <br>Duis varius eleifend ex. <br>Vivamus tristique faucibus tortor, in commodo felis posuere eu. <br>Sed aliquam tristique enim at volutpat. <br>Phasellus a aliquam quam, ac hendrerit libero. <br>Vivamus erat mi, mattis sit amet gravida id, malesuada ac lacus. <br>Ut commodo lobortis egestas. <br>Vivamus vulputate nisi non ligula ullamcorper pulvinar. <br>Phasellus tincidunt leo et venenatis auctor. <br>Proin ac urna et risus ullamcorper viverra. <br>Morbi non nunc at augue tincidunt scelerisque. <br>Proin vel ligula erat. <br>Aliquam dignissim pharetra leo, sit amet molestie sem efficitur non.
+ Donec mattis porttitor consectetur. <br>Nulla vulputate metus quis tellus pharetra, id maximus lacus mattis. <br>Donec a mi mattis, feugiat lectus a, rutrum leo. <br>Suspendisse et tellus urna. <br>Quisque porta ex at efficitur venenatis. <br>Sed volutpat malesuada mauris, sed dapibus dolor faucibus sed. <br>Pellentesque tristique tortor a nisl imperdiet convallis. <br>Curabitur ornare pharetra lacus at luctus. <br>
+ </div>
+ </div>
+
+ <br>
+
+ <br><br>
+
+ <a href="#bottom" id="to_bottom_anchor">click to scroll to bottom</a>
+ <a id="top"></a>
+
+ <br>
+ <iframe title="subframe" src="scrolling_subframe.html" id="subframe"> </iframe>
+
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+
+ <div id="lowerDiv" class="div" >
+ <div class="content"> Scroll inside me!</div>
+ </div>
+
+ <h1 id="middleHeading" > Middle </h1>
+ This is the middle anchor #1<a id="middleAnchor"></a>
+
+ <a href="#top">click to scroll to top</a>
+
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ This is the bottom anchor #3<a id="bottom"></a>
+
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/interactions/scrolling_subframe.html b/browser/components/places/tests/browser/interactions/scrolling_subframe.html
new file mode 100644
index 0000000000..55ad70f295
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/scrolling_subframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=windows-1252">
+ <style>
+ .div {
+ height: 250px;
+ width: 250px;
+ overflow: auto;
+ }
+ .content {
+ height: 800px;
+ width: 2000px;
+ background-color: coral;
+ }
+ </style>
+</head>
+<body>
+
+ Subframe Content<br>
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. <br> Maecenas vitae condimentum erat, vel luctus nulla. <br>Praesent nulla magna, rhoncus quis velit eu, dapibus efficitur lectus. <br>Duis varius eleifend ex. <br>Vivamus tristique faucibus tortor, in commodo felis posuere eu. <br>Sed aliquam tristique enim at volutpat. <br>Phasellus a aliquam quam, ac hendrerit libero. <br>Vivamus erat mi, mattis sit amet gravida id, malesuada ac lacus. <br>Ut commodo lobortis egestas. <br>Vivamus vulputate nisi non ligula ullamcorper pulvinar. <br>Phasellus tincidunt leo et venenatis auctor. <br>Proin ac urna et risus ullamcorper viverra. <br>Morbi non nunc at augue tincidunt scelerisque. <br>Proin vel ligula erat. <br>Aliquam dignissim pharetra leo, sit amet molestie sem efficitur non.
+ Donec mattis porttitor consectetur. <br>Nulla vulputate metus quis tellus pharetra, id maximus lacus mattis. <br>Donec a mi mattis, feugiat lectus a, rutrum leo. <br>Suspendisse et tellus urna. <br>Quisque porta ex at efficitur venenatis. <br>Sed volutpat malesuada mauris, sed dapibus dolor faucibus sed. <br>Pellentesque tristique tortor a nisl imperdiet convallis. <br>Curabitur ornare pharetra lacus at luctus. <br>
+
+</body>
+</html>