diff --git a/.gitignore b/.gitignore index 3e9c8d5ff..c4e3ce895 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,10 @@ yarn-error.log tools/i18n_results tools/leet/*.json -# Cliqz +## Cliqz cliqz/ -# Tools +## Tools tools/amo/*.zip ## JSDoc @@ -30,8 +30,11 @@ docs/ jsdocs/ out/ -#OSX +## Jest +coverage/ + +## OSX .DS_Store -#Other Files +## Other Files bug-list.md diff --git a/app/content-scripts/click_to_play.js b/app/content-scripts/click_to_play.js index 3164fc14b..95c329857 100644 --- a/app/content-scripts/click_to_play.js +++ b/app/content-scripts/click_to_play.js @@ -26,7 +26,6 @@ const { onMessage } = chrome.runtime; * @var {Object} initialized with an object with exported init as its property */ const Click2PlayContentScript = (function(win, doc) { - const C2P_DATA = {}; /** * Create element for the specified html tag * @memberof Click2PlayContentScript @@ -52,10 +51,9 @@ const Click2PlayContentScript = (function(win, doc) { } }; /** - * Helper called by applyC2P - * This function creates a fragment of DOM used for replacement - * of a social tracker. - * It also sets listeners to mouse 'click' events. + * Helper function called by applyC2P(). This function creates a + * DOM fragment used to replace a social tracker. It also sets + * listeners for mouse 'click' events. * @memberof Click2PlayContentScript * @package * @@ -66,13 +64,10 @@ const Click2PlayContentScript = (function(win, doc) { const buildC2P = function(c2pFrame, c2pAppDef, html) { c2pFrame.addEventListener('load', () => { const idoc = c2pFrame.contentDocument; - idoc.documentElement.innerHTML = html; - const image = idoc.getElementById('ghostery-button'); - if (c2pAppDef.button) { - c2pFrame.style.width = `${image.width}px`; - c2pFrame.style.height = `${image.height}px`; + c2pFrame.style.width = '30px'; + c2pFrame.style.height = '19px'; c2pFrame.style.border = '0px'; } else { c2pFrame.style.width = '100%'; @@ -125,33 +120,31 @@ const Click2PlayContentScript = (function(win, doc) { const applyC2P = function(app_id, c2p_app, html) { c2p_app.forEach((c2pAppDef, idx) => { const els = doc.querySelectorAll(c2pAppDef.ele); - for (let i = 0, num_els = els.length; i < num_els; i++) { + for (let i = 0; i < els.length; i++) { const el = els[i]; - const c2pFrame = createEl('iframe'); + buildC2P(c2pFrame, c2pAppDef, html[idx]); c2pFrame.style.display = 'inline-block'; - if ((c2pAppDef.attach && c2pAppDef.attach === 'parentNode') || - (el.nodeName === 'IFRAME')) { + // Attach C2P frame inside the parentNode + if ((c2pAppDef.attach && c2pAppDef.attach === 'parentNode') || (el.nodeName === 'IFRAME')) { if (el.parentNode && el.parentNode.nodeName !== 'BODY' && el.parentNode.nodeName !== 'HEAD') { el.parentNode.replaceChild(c2pFrame, el); - return; } + } else { + // Replace existing node with C2P content + el.textContent = ''; + el.style.display = 'inline-block'; + appendChild(el, c2pFrame); } - - el.textContent = ''; - - el.style.display = 'inline-block'; - appendChild(el, c2pFrame); } }); }; /** - * Initialize Click2PlayContentScript. - * This function sets listener for 'c2p' message coming from background with - * tracker-related data. It also sets listener for windows 'load' event. - * Called by exported init function. + * Initialize Click2PlayContentScript. This function sets a listener for 'c2p' messages + * with tracker-related data. This script is injected on document_idle after DOM complete. + * Called by exported init() function. * @memberof Click2PlayContentScript * @package */ @@ -161,34 +154,24 @@ const Click2PlayContentScript = (function(win, doc) { return false; } - const { name, message } = request; + const { name, message } = request; log('click_to_play.js received message', name); if (name === 'c2p') { if (message) { - // queue Click-to-Play data so that we process multiple Twitter buttons at once, for example - C2P_DATA[message.app_id] = [message.app_id, message.data, message.html]; - - if (doc.readyState === 'complete') { - applyC2P(message.app_id, message.data, message.html); + // Dequeue C2P data stored while the script injection was taking place + for (const app_id in message) { + if (message.hasOwnProperty(app_id)) { + applyC2P(app_id, message[app_id].data, message[app_id].html); + delete message[app_id]; + } } } } sendResponse(); - return true; + return false; }); - - window.addEventListener('load', () => { - for (const app_id in C2P_DATA) { - if (C2P_DATA.hasOwnProperty(app_id)) { - if (C2P_DATA[app_id].length >= 3) { - applyC2P(C2P_DATA[app_id][0], C2P_DATA[app_id][1], C2P_DATA[app_id][2]); - } - } - } - // TODO clear C2P_DATA to free memory - }, { capture: false, passive: true }); }; // Public API diff --git a/app/content-scripts/notifications.js b/app/content-scripts/notifications.js index 1756904e0..a7cf4dc1d 100644 --- a/app/content-scripts/notifications.js +++ b/app/content-scripts/notifications.js @@ -768,7 +768,7 @@ const NotificationsContentScript = (function(win, doc) { exportFile(message); } - // trigger a response callback to src/background so that we can handler errors properly + // trigger a response callback to src/background so that we can handle errors properly sendResponse(); return true; }); diff --git a/app/scss/partials/_blocking_tracker.scss b/app/scss/partials/_blocking_tracker.scss index c036a20fe..b0394175f 100644 --- a/app/scss/partials/_blocking_tracker.scss +++ b/app/scss/partials/_blocking_tracker.scss @@ -167,10 +167,10 @@ } .cliqz-tracker-trust > g > path:nth-child(1) { - stroke: #00AEF0; + stroke: #d8d8d8; } .cliqz-tracker-trust > g > path:nth-child(2) { - fill: #00AEF0; + fill: #f7f7f7; } .cliqz-tracker-scrub > g > .border { fill: #FFF; diff --git a/src/classes/EventHandlers.js b/src/classes/EventHandlers.js index 55ba5f4e1..f84b7a6db 100644 --- a/src/classes/EventHandlers.js +++ b/src/classes/EventHandlers.js @@ -694,7 +694,7 @@ class EventHandlers { }; } } else if (fromRedirect) { - const url = buildRedirectC2P(requestId, globals.REDIRECT_MAP.get(requestId), appId); + const url = buildRedirectC2P(globals.REDIRECT_MAP.get(requestId), appId); setTimeout(() => { chrome.tabs.update(details.tabId, { url }); }, 0); @@ -878,7 +878,7 @@ class EventHandlers { * */ _resetNotifications() { - globals.C2P_LOADED = globals.NOTIFICATIONS_LOADED = false; // eslint-disable-line no-multi-assign + globals.NOTIFICATIONS_LOADED = false; } } diff --git a/src/classes/Globals.js b/src/classes/Globals.js index cd56ccdb1..0d8894f93 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -41,7 +41,6 @@ class Globals { this.REQUIRE_LEGACY_OPT_IN = false; this.HOTFIX = false; this.LET_REDIRECTS_THROUGH = false; - this.C2P_LOADED = false; this.NOTIFICATIONS_LOADED = false; this.upgrade_alert_shown = false; diff --git a/src/classes/TabInfo.js b/src/classes/TabInfo.js index 9d3e60559..accadfdea 100644 --- a/src/classes/TabInfo.js +++ b/src/classes/TabInfo.js @@ -2,6 +2,8 @@ * TabInfo Class * * this._tabInfo[tab_id]: { + * c2pStatus {string} current status of the click_to_play.js script injection on the tab (none|loading|done) + * c2pQueue {Object} queue of c2p messages collected when c2pStatus is 'none' or 'loading', organized as a HashSet by app_id * domain: {string} the general domain name plus suffix (no sub-domains) * hash: {string} hash values appended to the url * host: {string} the domain name plus suffix and sub-domains @@ -46,7 +48,7 @@ class TabInfo { /** * Create a new _tabInfo object - * @param {number} tab_id tab id + * @param {number} tab_id tab id * @param {string} tab_url tab url */ create(tab_id, tab_url) { @@ -68,6 +70,8 @@ class TabInfo { unblocked: {}, }, insecureRedirects: [], + c2pStatus: 'none', + c2pQueue: {}, }; this._tabInfo[tab_id] = info; @@ -76,12 +80,12 @@ class TabInfo { /** * Getter method - * @param {number} tab_id tab id + * @param {number} tab_id tab id * @param {string} property property name * @return {Object} _tabInfo data */ - // TODO consider improving handling of what if we mistype the property name. - // always returning object where property might sometimes have returned false could result in subtle bugs. + // TODO consider improving handling of what happens if we mistype the property name. Always + // returning an object where property would otherwise have returned false could result in subtle bugs. getTabInfo(tab_id, property) { if (this._tabInfo.hasOwnProperty(tab_id)) { if (property) { @@ -94,8 +98,8 @@ class TabInfo { /** * Getter method for tab parameters which we want to persist during the session. - * @param {number} tab_id tab id - * @param {string} property persitant property name + * @param {number} tab_id tab id + * @param {string} property persistent property name * @return {Object} persistent data for this tab */ getTabInfoPersist(tab_id, property) { @@ -167,7 +171,7 @@ class TabInfo { * * @private * - * @param {number} tab_id tab id + * @param {number} tab_id tab id * @param {string} tab_url tab url */ _updateUrl(tab_id, tab_url) { diff --git a/src/utils/click2play.js b/src/utils/click2play.js index 5e433db32..abd0ec7ce 100644 --- a/src/utils/click2play.js +++ b/src/utils/click2play.js @@ -40,8 +40,14 @@ const policy = new Policy(); */ export function buildC2P(details, app_id) { const { tab_id } = details; - let c2pApp = c2pDb.db.apps && c2pDb.db.apps[app_id]; + const tab = tabInfo.getTabInfo(tab_id); + // If the tab is prefetched, a chrome newtab or Firefox about:page, we can't add C2P to it + if (!tab || tab.prefetched || tab.path.includes('_/chrome/newtab') || tab.protocol === 'about' || globals.EXCLUDES.includes(tab.host)) { + return; + } + + let c2pApp = c2pDb.db.apps && c2pDb.db.apps[app_id]; if (!c2pApp) { return; } @@ -56,8 +62,7 @@ export function buildC2P(details, app_id) { } const app_name = bugDb.db.apps[app_id].name; const c2pHtml = []; - const tab_host = tabInfo.getTabInfo(tab_id, 'host'); - const blacklisted = !!policy.blacklisted(tab_host); + const blacklisted = !!policy.blacklisted(tab.host); // Generate the templates for each c2p definition (could be multiple for an app ID) c2pApp.forEach((c2pAppDef) => { @@ -88,33 +93,70 @@ export function buildC2P(details, app_id) { c2pHtml.push(c2p_tpl({ data: tplData })); }); - if (app_id === 2575) { // Hubspot forms. Adjust selector. + // Hubspot forms. Adjust selector + if (app_id === 2575) { c2pApp.ele = _getHubspotFormSelector(details.url); } - // TODO top-level documents only for now - _injectClickToPlay(tab_id).then((result) => { - if (result) { + + // Make sure that the click_to_play.js content script has loaded on the + // top-level document before sending c2p data to the page + switch (tab.c2pStatus) { + case 'none': + tabInfo.setTabInfo(tab_id, 'c2pStatus', 'loading'); + // Push current C2P data into existing queue + if (!tab.c2pQueue.hasOwnProperty(app_id)) { + tabInfo.setTabInfo(tab_id, 'c2pQueue', Object.assign({}, tab.c2pQueue, { + [app_id]: { + data: c2pApp, + html: c2pHtml + } + })); + } + // Scripts injected at document_idle are guaranteed to run after the DOM is complete + injectScript(tab_id, 'dist/click_to_play.js', '', 'document_idle').then(() => { + // Send the entire queue to the content script to reduce message passing + sendMessage(tab_id, 'c2p', tab.c2pQueue); + tabInfo.setTabInfo(tab_id, 'c2pStatus', 'done'); + tabInfo.setTabInfo(tab_id, 'c2pQueue', {}); + }).catch((err) => { + log('buildC2P error', err); + }); + break; + case 'loading': + // Push C2P data to a holding queue until click_to_play.js has finished loading on the page + if (!tab.c2pQueue.hasOwnProperty(app_id)) { + tabInfo.setTabInfo(tab_id, 'c2pQueue', Object.assign({}, tab.c2pQueue, { + [app_id]: { + data: c2pApp, + html: c2pHtml + } + })); + } + break; + case 'done': sendMessage(tab_id, 'c2p', { - app_id, - data: c2pApp, - html: c2pHtml - // tabWindowId: message.tabWindowId + app_id: { + data: c2pApp, + html: c2pHtml + } }); - } - }); + break; + default: + log(`buildC2P error: c2pStatus type ${tab.c2pStatus} not matched`); + } } /** - * Build blocked redirect data global structure Inject Page-Level Click2Play on Redirect. + * Inject page-level Click2Play on redirect. Build blocked redirect + * data global structure. * @memberOf BackgroundUtils * - * @param {number} requestId request id - * @param {Object} redirectUrls original url and redirect url as properties - * @param {number} app_id tracker id + * @param {Object} redirectUrls original url and redirect url as properties + * @param {number} app_id tracker id * * @return {string} url of the internal template of the blocked redirect page */ -export function buildRedirectC2P(requestId, redirectUrls, app_id) { +export function buildRedirectC2P(redirectUrls, app_id) { const host_url = processUrl(redirectUrls.url).hostname; const redirect_url = processUrl(redirectUrls.redirectUrl).hostname; const app_name = bugDb.db.apps[app_id].name; @@ -129,7 +171,7 @@ export function buildRedirectC2P(requestId, redirectUrls, app_id) { blocked_redirect_prevent: t( 'blocked_redirect_prevent', // It is unlikely that apps pages will ever be translated - // [host_url, redirect_url, app_name, 'https://' + globals.APPS_SUB_DOMAIN + '.ghostery.com/' + conf.language + '/apps/' + encodeURIComponent(app_name.replace(/\s+/g, '_').toLowerCase())]), + // [host_url, redirect_url, app_name, 'https://' + globals.APPS_SUB_DOMAIN + '.ghostery.com/' + conf.language + '/apps/' + encodeURIComponent(app_name.replace(/\s+/g, '_').toLowerCase())]), [host_url, redirect_url, app_name, `${globals.APPS_BASE_URL}/en/apps/${encodeURIComponent(app_name.replace(/\s+/g, '_').toLowerCase())}`] ), blocked_redirect_action_always_title: t('blocked_redirect_action_always_title'), @@ -152,12 +194,11 @@ export function allowAllwaysC2P(app_id, tab_host) { delete selected_app_ids[app_id]; conf.selected_app_ids = selected_app_ids; - // Remove fron site-specific-blocked if (conf.site_specific_blocks.hasOwnProperty(tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { const index = conf.site_specific_blocks[tab_host].indexOf(+app_id); const { site_specific_blocks } = conf; - site_specific_blocks[tab_host].splice(index); + site_specific_blocks[tab_host].splice(0, 1); conf.site_specific_blocks = site_specific_blocks; } @@ -193,30 +234,3 @@ function _getHubspotFormSelector(url) { const tokens = url.substr(8).split(/\/|\&|\?|\#|\=/ig); // eslint-disable-line no-useless-escape return `form[id="hsForm_${tokens[5]}"]`; } - -/** - * Inject dist/click_to_play.js content script - * @private - * - * @param {number} tab_id tab id - * @return {Promise} true/false - */ -function _injectClickToPlay(tab_id) { - if (globals.C2P_LOADED) { - return Promise.resolve(true); - } - - const tab = tabInfo.getTabInfo(tab_id); - if (!tab || tab.prefetched || tab.path.includes('_/chrome/newtab') || tab.protocol === 'about' || globals.EXCLUDES.includes(tab.host)) { - // If the tab is prefetched, a chrome newtab or Firefox about:page, we can't add C2P to it. - return Promise.resolve(true); - } - - return injectScript(tab_id, 'dist/click_to_play.js', '', 'document_end').then(() => { - globals.C2P_LOADED = true; - return true; - }).catch((err) => { - log('_injectClickToPlay error', err); - return false; // prevent sendMessage calls - }); -} diff --git a/src/utils/matcher.js b/src/utils/matcher.js index 796802507..0e3118639 100644 --- a/src/utils/matcher.js +++ b/src/utils/matcher.js @@ -81,7 +81,6 @@ export function fuzzyUrlMatcher(url, urls) { for (let i = 0; i < urls.length; i++) { const { host, path } = processFpeUrl(urls[i]); - if (host === tab_host) { if (!path) { log(`[fuzzyUrlMatcher] host (${host}) match`); @@ -207,7 +206,7 @@ function _matchesRegex(src) { } /** - * Match a path part of a url agains the path property of database patterns section. + * Match a path part of a url against the path property of database patterns section. * @private * * @param {string} src_path path part of an url diff --git a/src/utils/utils.js b/src/utils/utils.js index faa6bae12..363753e70 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -540,7 +540,7 @@ export function fetchLocalJSONResource(url) { } /** - * Inject content scripts and CSS into a given tabID. + * Inject content scripts and CSS into a given tabID (top-level frame only). * Note: Chrome 61 blocks content scripts on the new tab page (_/chrome/newtab). Be * sure to check the current URL before calling this function, otherwise Chrome will throw * a permission error diff --git a/test/utils/click2play.test.js b/test/utils/click2play.test.js new file mode 100644 index 000000000..6ad718ec5 --- /dev/null +++ b/test/utils/click2play.test.js @@ -0,0 +1,225 @@ +/** + * click2play.js Unit Tests + * + * Ghostery Browser Extension + * http://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import { buildC2P, buildRedirectC2P, allowAllwaysC2P } from '../../src/utils/click2play'; +import tabInfo from '../../src/classes/TabInfo'; +import Policy from '../../src/classes/Policy'; +import globals from '../../src/classes/Globals'; +import conf from '../../src/classes/Conf'; +import c2p_tpl from '../../app/templates/click2play.html'; +import * as utils from '../../src/utils/utils'; + +// Mock imports for dependencies +jest.mock('../../app/templates/click2play.html', () => { + const _ = require('underscore'); + return _.template('../../app/templates/click2play.html'); +}); +jest.mock('../../src/classes/TabInfo', () => ({ + c2pStatus: 'none', + c2pQueue: {}, + host: 'cnn.com', + path: '', + prefetched: false, + protocol: 'https', +})); +jest.mock('../../src/classes/Policy'); +jest.mock('../../src/classes/Globals', () => ({ + BROWSER_INFO: { displayName: '', name: '', token: '', version: '', os: 'other' }, + EXCLUDES: [], +})); +jest.mock('../../src/classes/Click2PlayDb', () => ({ + type: 'click2play', + db: { + apps: { + 464: [{ + aid: 464, + allow: [464, 93, 922], + frameColor: '', + text: '', + button: 'ghostery_facebook.png', + attach: false, + ele: 'iframe[src*=facebook\\.com\\/plugins\\/like\\.php], .fb-like, fb\\:like', + type: '' + },{ + aid: 464, + allow: [464, 93, 922], + frameColor: '', + text: '', + button: '', + attach: 'parentNode', + ele: 'iframe[src*=facebook\\.com\\/plugins\\/video\\.php], iframe[src*=facebook\\.com\\/plugins\\/likebox\\.php], iframe[src*=facebook\\.com\\/plugins\\/post\\.php], iframe[src*=facebook\\.com\\/plugins\\/activity\\.php], .fb-like-box, fb\\:like-box, .fb-activity, fb\\:activity', + type: '' + },{ + aid: 464, + allow: [464, 93, 922], + frameColor: '', + text: '', + button: '', + attach: false, + ele: 'fb\\:comments, #fb-comments', + type: '' + }], + } + } +})); +jest.mock('../../src/classes/BugDb', () => ({ + db: { + apps: { + 464: { + name: "Facebook Social Plugins" + }, + } + } +})); +jest.mock('../../src/classes/Conf', () => ({ + selected_app_ids: { + 15: 1, + 41: 1, + }, + site_specific_blocks: { + 'www.ghostery.com': [15, 100], + 'www.cnn.com': [41], + }, + site_specific_unblocks: { + 'www.cnn.com': [15, 50], + }, +})); + +// Mock TabInfo data +tabInfo.getTabInfo = jest.fn(); +tabInfo.getTabInfo.mockReturnValue(tabInfo); +tabInfo.setTabInfo = jest.fn().mockImplementation((tab_id, property, value) => { + tabInfo[property] = value; +}); + +// Mock utils functions +utils.sendMessage = jest.fn(); +utils.injectScript = jest.fn(() => Promise.resolve()); +utils.processUrl = jest.requireActual('../../src/utils/utils').processUrl; + +describe('src/utils/click2play.js', () => { + const details = { + tab_id: 1 + }; + const tab = tabInfo.getTabInfo(details.tab_id); + + describe('testing buildC2P()', () => { + describe('c2pStatus is "none"', () => { + beforeAll(() => { + tabInfo.c2pStatus = 'none'; + utils.sendMessage.mockClear(); + utils.injectScript.mockClear(); + }); + + test('c2pStatus defaults to "none"', () => { + expect(tab.c2pStatus).toBe('none'); + }); + + test('injectScript() is called', () => { + buildC2P(details, 464); + expect(utils.injectScript).toHaveBeenCalledWith(details.tab_id, 'dist/click_to_play.js', '', 'document_idle'); + }); + + test('c2pApp and c2pHtml data added to c2pQueue', () => { + // Look at what data was added to c2pQueue via setTabInfo + const c2pQueue = tabInfo.setTabInfo.mock.calls[1][2]; + expect(c2pQueue).toHaveProperty('464'); + }); + + test('sendMessage() called with correct C2P data', () => { + const c2pQueue = tabInfo.setTabInfo.mock.calls[1][2]; + expect(utils.sendMessage).toHaveBeenCalledWith(details.tab_id, 'c2p', c2pQueue); + }); + + test('c2pStatus set to "done"', () => { + expect(tab.c2pStatus).toBe('done'); + }); + + test('c2pQueue cleared', () => { + expect(tab.c2pQueue).toEqual({}); + }); + }); + + describe('c2pStatus is "loading"', () => { + beforeAll(() => { + utils.sendMessage.mockClear(); + utils.injectScript.mockClear(); + tabInfo.c2pStatus = 'loading'; + buildC2P(details, 464); + }); + + test('injectScript() and sendMessage() are not called', () => { + expect(utils.injectScript).not.toHaveBeenCalled(); + expect(utils.sendMessage).not.toHaveBeenCalled(); + }); + + test('c2pApp and c2pHtml data added to c2pQueue', () => { + expect(tab.c2pQueue).toHaveProperty('464'); + }); + }); + + describe('c2pStatus is "done"', () => { + beforeAll(() => { + utils.sendMessage.mockClear(); + utils.injectScript.mockClear(); + tabInfo.c2pStatus = 'done'; + tabInfo.c2pQueue = {}; + buildC2P(details, 464); + }); + + test('injectScript() is not called. sendMessage() is called', () => { + expect(utils.injectScript).not.toHaveBeenCalled(); + expect(utils.sendMessage).toHaveBeenCalled(); + }); + + test('c2pQueue is empty', () => { + expect(tab.c2pQueue).toEqual({}); + }); + }); + }); + + describe('testing buildRedirectC2P()', () => { + const REDIRECT_MAP = new Map([[100, { url: 'https://cnn.com/', redirectUrl: 'https://fake-redirect.com/' }]]); + + test('app_id is added to BLOCKED_REDIRECT_DATA global', () => { + buildRedirectC2P(REDIRECT_MAP.get(100), 464); + expect(globals.BLOCKED_REDIRECT_DATA.app_id).toBe(464); + expect(globals.BLOCKED_REDIRECT_DATA.url).toBe('https://fake-redirect.com/'); + }); + }); + + describe('testing allowAllwaysC2P()', () => { + test('app_id is removed from selected_app_ids', () => { + allowAllwaysC2P(15, 'www.espn.com'); + expect(conf.selected_app_ids).not.toHaveProperty('15'); + expect(conf.selected_app_ids).toMatchObject({41: 1}); + }); + + test('app_id is removed from site_specific_blocks', () => { + allowAllwaysC2P(15, 'www.ghostery.com'); + expect(conf.site_specific_blocks['www.ghostery.com']).toEqual(expect.not.arrayContaining([15])); + expect(conf.site_specific_blocks['www.ghostery.com']).toEqual(expect.arrayContaining([100])); + }); + + test('app_id is added to site_specific_unblocks', () => { + allowAllwaysC2P(41, 'www.cnn.com'); + expect(conf.site_specific_unblocks['www.cnn.com']).toEqual(expect.arrayContaining([15,50,41])); + // Check results from preceding tests + expect(conf.site_specific_unblocks['www.espn.com']).toEqual(expect.arrayContaining([15])); + expect(conf.site_specific_unblocks['www.ghostery.com']).toEqual(expect.arrayContaining([15])); + // Check removal from selected_app_ids and site_specific_blocks as well + expect(conf.selected_app_ids).not.toHaveProperty('41'); + expect(conf.site_specific_blocks['www.cnn.com']).toEqual(expect.not.arrayContaining([41])); + }); + }); +}); diff --git a/test/utils/matcher.test.js b/test/utils/matcher.test.js index a2286d925..87dc6f432 100644 --- a/test/utils/matcher.test.js +++ b/test/utils/matcher.test.js @@ -13,7 +13,7 @@ import bugDb from '../../src/classes/BugDb'; import conf from '../../src/classes/Conf'; -import { isBug } from '../../src/utils/matcher'; +import { isBug, fuzzyUrlMatcher } from '../../src/utils/matcher'; describe('src/utils/matcher.js', () => { beforeAll(done => { @@ -65,6 +65,9 @@ describe('src/utils/matcher.js', () => { }, "path": { "js/tracking.js": 13 + }, + "regex": { + 15: "(googletagservices\\.com\\/.*\\.js)" } } }); @@ -86,16 +89,25 @@ describe('src/utils/matcher.js', () => { describe('testing isBug()', () => { describe('testing basic pattern matching', () => { + test('host only tracker matching works', () => { + expect(isBug('https://gmodules.com/', 'example.com')).toBe(101); + }); + test('host+path tracker matching works', () => { - return expect(isBug('https://apis.google.com/js/plusone.js', 'example.com')).toBe(1240); + expect(isBug('https://apis.google.com/js/plusone.js', 'example.com')).toBe(1240); }); test('path only tracker matching works', () => { - return expect(isBug('https://apis.google.com/js/tracking.js', 'example.com')).toBe(13); + expect(isBug('https://apis.google.com/js/tracking.js', 'example.com')).toBe(13); + }); + + test('regex tracker matching works', () => { + expect(isBug('https://apis.google.com/js/tracking.js', 'example.com')).toBe(13); }); test('pattern matching is case insensitive', () => { - return expect(isBug('https://APIS.Google.com/js/Tracking.js', 'example.com')).toBe(13); + expect(isBug('https://googletagservices.com/anything/tracker.js', 'example.com')).toBe(15); + expect(isBug('https://googletagservices.com/anything/tracker.css', 'example.com')).toBeFalsy(); }); }); @@ -103,15 +115,15 @@ describe('src/utils/matcher.js', () => { const twitter_button = 'http://platform.twitter.com/widgets/'; test('first confirm Twitter Button is a tracker', () => { - return expect(isBug(twitter_button)).toBe(991); + expect(isBug(twitter_button)).toBe(991); }); test('host-only first-party exception', () => { - return expect(isBug(twitter_button, 'https://twitter.com/ghostery')).toBeFalsy(); + expect(isBug(twitter_button, 'https://twitter.com/ghostery')).toBeFalsy(); }); test('same exception on the same page URL as above, but with www', () => { - return expect(isBug(twitter_button, 'https://www.twitter.com/ghostery')).toBeFalsy(); + expect(isBug(twitter_button, 'https://www.twitter.com/ghostery')).toBeFalsy(); }); }); @@ -119,11 +131,11 @@ describe('src/utils/matcher.js', () => { const google_widgets = 'http://gmodules.com/blah'; test('first confirm Google Widgets is a tracker', () => { - return expect(isBug(google_widgets)).toBe(101); + expect(isBug(google_widgets)).toBe(101); }); test('host and exact path exception', () => { - return expect(isBug(google_widgets, 'http://google.com/ig')).toBeFalsy(); + expect(isBug(google_widgets, 'http://google.com/ig')).toBeFalsy(); }); }); @@ -131,12 +143,31 @@ describe('src/utils/matcher.js', () => { const google_plus_one = 'https://apis.google.com/js/plusone.js'; test('first confirm Google +1 is a tracker', () => { - return expect(isBug(google_plus_one)).toBe(1240); + expect(isBug(google_plus_one)).toBe(1240); }); test('host and fuzzy path exception', () => { - return expect(isBug(google_plus_one, 'https://chrome.google.com/webstore/detail/ghostery/mlomiejdfkolichcflejclcbmpeaniij?hl=en')).toBeFalsy(); + expect(isBug(google_plus_one, 'https://chrome.google.com/webstore/detail/ghostery/mlomiejdfkolichcflejclcbmpeaniij?hl=en')).toBeFalsy(); }); }); }); + + describe('testing fuzzyUrlMatcher()', () => { + const urls = ['google.com', 'ghostery.com/products', 'example.com/page*']; + + test('host match', () => { + expect(fuzzyUrlMatcher('https://google.com/analytics', urls)).toBeTruthy(); + expect(fuzzyUrlMatcher('https://analytics.google.com/something', urls)).toBeFalsy(); + }); + + test('host and path fuzzy match', () => { + expect(fuzzyUrlMatcher('https://example.com/page_anything', urls)).toBeTruthy(); + expect(fuzzyUrlMatcher('https://example.com/p', urls)).toBeFalsy(); + }); + + test('host and path match', () => { + expect(fuzzyUrlMatcher('https://ghostery.com/products', urls)).toBeTruthy(); + expect(fuzzyUrlMatcher('https://ghostery.com/products1', urls)).toBeFalsy(); + }); + }); }); diff --git a/test/utils/utils.test.js b/test/utils/utils.test.js index ebae5cdf7..a622547b3 100644 --- a/test/utils/utils.test.js +++ b/test/utils/utils.test.js @@ -11,7 +11,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ -import { getJson, defineLazyProperty, semverCompare } from '../../src/utils/utils'; +import { getJson, defineLazyProperty, processFpeUrl, semverCompare } from '../../src/utils/utils'; describe('tests for getJson()', () => { // Tests for getJson() @@ -62,6 +62,15 @@ describe('tests for defineLazyProperty()', () => { test('property function is still lazy', () => expect(neverCalledSpy).not.toHaveBeenCalled()); }); +describe('test for processFpeUrl()', () => { + test('host only', () => { + expect(processFpeUrl('ghostery.com')).toMatchObject({host: 'ghostery.com', path: ''}); + }); + test('host and path', () => { + expect(processFpeUrl('ghostery.com/products')).toMatchObject({host: 'ghostery.com', path: 'products'}); + }); +}); + describe('tests for semverCompare()', () => { const versions = [ '1.2.1',