diff options
Diffstat (limited to 'browser/components/places/tests/browser/interactions')
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> |