Skip to content

context menu fix for cosmetic filter #2486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cosmeticFilterActions from '../actions/cosmeticFilterActions'

let rule = {
export let rule = {
host: '',
selector: ''
}
Expand Down Expand Up @@ -30,24 +30,28 @@ chrome.contextMenus.create({
parentId: 'brave',
contexts: ['all']
})
// context menu listener emit event -> query -> tabsCallback -> onSelectorReturned

// contextMenu listener - when triggered, grab latest selector
chrome.contextMenus.onClicked.addListener(function (info, tab) {
switch (info.menuItemId) {
case 'addBlockElement': {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs: any) {
chrome.tabs.sendMessage(tabs[0].id, { type: 'getTargetSelector' }, function (response: any) {
if (response) {
rule.selector = window.prompt('CSS selector to block: ', `${response}`) || ''
chrome.tabs.insertCSS({
code: `${rule.selector} {display: none;}`
})
cosmeticFilterActions.siteCosmeticFilterAdded(rule.host, rule.selector)
}
})
})
chrome.contextMenus.onClicked.addListener((info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) => {
onContextMenuClicked(info, tab)
})

// content script listener for right click DOM selection event
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const action = typeof msg === 'string' ? msg : msg.type
switch (action) {
case 'contextMenuOpened': {
rule.host = msg.baseURI
break
}
}
})

export function onContextMenuClicked (info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) {
switch (info.menuItemId) {
case 'addBlockElement':
query()
break
case 'resetSiteFilterSettings': {
cosmeticFilterActions.siteCosmeticFilterRemoved(rule.host)
break
Expand All @@ -60,15 +64,29 @@ chrome.contextMenus.onClicked.addListener(function (info, tab) {
console.warn('[cosmeticFilterEvents] invalid context menu option: ${info.menuItemId}')
}
}
})
}

// content script listener for right click DOM selection event
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const action = typeof msg === 'string' ? msg : msg.type
switch (action) {
case 'contextMenuOpened': {
rule.host = msg.baseURI
break
}
export function query () {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs: [chrome.tabs.Tab]) => {
tabsCallback(tabs)
})
}

export function tabsCallback (tabs: any) {
chrome.tabs.sendMessage(tabs[0].id, { type: 'getTargetSelector' }, onSelectorReturned)
}

export function onSelectorReturned (response: any) {
if (!response) {
rule.selector = window.prompt('We were unable to automatically populate a correct CSS selector for you. Please manually enter a CSS selector to block:') || ''
} else {
rule.selector = window.prompt('CSS selector:', `${response}`) || ''
}
})

if (rule.selector && rule.selector.length > 0) {
chrome.tabs.insertCSS({
code: `${rule.selector} {display: none;}`
})
cosmeticFilterActions.siteCosmeticFilterAdded(rule.host, rule.selector)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/* 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 cosmeticFilterActions from '../../../../brave_extension/extension/brave_extension/background/actions/cosmeticFilterActions'
import * as cosmeticFilterEvents from '../../../../brave_extension/extension/brave_extension/background/events/cosmeticFilterEvents'

let lastInputText: string
let lastPromptText: string
let selectorToReturn: string

global.prompt = (inputText: string, promptText: string) => {
lastInputText = inputText
lastPromptText = promptText
return selectorToReturn
}

describe('cosmeticFilterEvents events', () => {
describe('when runtime.onMessage is received', () => {
describe('contextMenuOpened', () => {
it('assigns the base URI', () => {
chrome.runtime.sendMessage({ type: 'contextMenuOpened', baseURI: 'brave.com' },
() => {
expect(cosmeticFilterEvents.rule.host).toBe('brave.com')
})
})
})
})

describe('chrome.contextMenus.onClicked listener', () => {
let contextMenuOnClickedSpy: jest.SpyInstance
let chromeTabsQuerySpy: jest.SpyInstance
let resetSiteFilterSettingsSpy: jest.SpyInstance
let resetAllFilterSettingsSpy: jest.SpyInstance
let chromeTabsSendMessageSpy: jest.SpyInstance
beforeEach(() => {
contextMenuOnClickedSpy = jest.spyOn(chrome.tabs, 'create')
chromeTabsQuerySpy = jest.spyOn(chrome.tabs, 'query')
resetSiteFilterSettingsSpy = jest.spyOn(cosmeticFilterActions, 'siteCosmeticFilterRemoved')
resetAllFilterSettingsSpy = jest.spyOn(cosmeticFilterActions, 'allCosmeticFiltersRemoved')
chromeTabsSendMessageSpy = jest.spyOn(chrome.tabs, 'sendMessage')
})
afterEach(() => {
contextMenuOnClickedSpy.mockRestore()
chromeTabsSendMessageSpy.mockRestore()
})

describe('addBlockElement', function () {
it('triggers addBlockElement action (query call)', function () {
const info: chrome.contextMenus.OnClickData = { menuItemId: 'addBlockElement', editable: false, pageUrl: 'brave.com' }
// calls query
const tab: chrome.tabs.Tab = {
id: 3,
index: 0,
pinned: false,
highlighted: false,
windowId: 1,
active: true,
incognito: false,
selected: true,
discarded: false,
autoDiscardable: false
}
cosmeticFilterEvents.onContextMenuClicked(info, tab)
expect(chromeTabsQuerySpy).toBeCalled()
})
it('calls tabsCallback', function () {
const myTab: chrome.tabs.Tab = {
id: 3,
index: 0,
pinned: false,
highlighted: false,
windowId: 1,
active: true,
incognito: false,
selected: true,
discarded: false,
autoDiscardable: false
}
cosmeticFilterEvents.tabsCallback([myTab])
expect(1).toBe(1)
chrome.tabs.sendMessage(myTab.id, { type: 'getTargetSelector' }, cosmeticFilterEvents.onSelectorReturned)
})
})
describe('resetSiteFilterSettings', function () {
it('triggers `siteCosmeticFilterRemoved` action', function () {
const info: chrome.contextMenus.OnClickData = { menuItemId: 'resetSiteFilterSettings', editable: false, pageUrl: 'brave.com' }
const tab: chrome.tabs.Tab = {
id: 3,
index: 0,
pinned: false,
highlighted: false,
windowId: 1,
active: true,
incognito: false,
selected: true,
discarded: false,
autoDiscardable: false
}
cosmeticFilterEvents.onContextMenuClicked(info, tab)
expect(resetSiteFilterSettingsSpy).toBeCalled()
})
})
describe('resetAllFilterSettings', function () {
it('triggers `allCosmeticFiltersRemoved` action', function () {
const info: chrome.contextMenus.OnClickData = { menuItemId: 'resetAllFilterSettings', editable: false, pageUrl: 'brave.com' }
const tab: chrome.tabs.Tab = {
id: 3,
index: 0,
pinned: false,
highlighted: false,
windowId: 1,
active: true,
incognito: false,
selected: true,
discarded: false,
autoDiscardable: false
}
cosmeticFilterEvents.onContextMenuClicked(info, tab)
expect(resetAllFilterSettingsSpy).toBeCalled()
})
})
describe('onSelectorReturned', function () {
describe('when prompting user with selector', function () {
describe('when a selector is returned', function () {
it('calls window.prompt with selector as input', function () {
cosmeticFilterEvents.onSelectorReturned('abc')
expect(lastInputText).toBe('CSS selector:')
expect(lastPromptText).toBe('abc')
})
})
describe('when a selector is not returned', function () {
it('calls window.prompt with `not found` message', function () {
cosmeticFilterEvents.onSelectorReturned(null)
expect(lastInputText.indexOf('We were unable to automatically populat') > -1).toBe(true)
})
})
})
describe('after selector prompt is shown', function () {
let insertCssSpy: jest.SpyInstance
beforeEach(() => {
insertCssSpy = jest.spyOn(chrome.tabs, 'insertCSS')
})
afterEach(() => {
insertCssSpy.mockRestore()
})
it('calls `chrome.tabs.insertCSS` when selector is NOT null/undefined', function () {
selectorToReturn = '#test_selector'
cosmeticFilterEvents.onSelectorReturned(selectorToReturn)
let returnObj = {
'code': '#test_selector {display: none;}'
}
expect(insertCssSpy).toBeCalledWith(returnObj)
})
it('does NOT call `chrome.tabs.insertCSS` when selector is undefined', function () {
selectorToReturn = undefined
cosmeticFilterEvents.onSelectorReturned(undefined)
expect(insertCssSpy).not.toBeCalled()
})
it('does NOT call `chrome.tabs.insertCSS` when selector is null', function () {
selectorToReturn = null
cosmeticFilterEvents.onSelectorReturned(null)
expect(insertCssSpy).not.toBeCalled()
})
})
})
})
})
40 changes: 38 additions & 2 deletions components/test/testData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,13 @@ export const blockedResource: BlockDetails = {
subresource: 'https://www.brave.com/test'
}

// see: https://developer.chrome.com/extensions/events
interface OnMessageEvent extends chrome.events.Event<(message: object, options: any, responseCallback: any) => void> {
emit: (message: object) => void
}

export const getMockChrome = () => {
return {
let mock = {
send: () => undefined,
getVariableValue: () => undefined,
braveRewards: {
Expand All @@ -116,7 +121,13 @@ export const getMockChrome = () => {
onConnect: new ChromeEvent(),
onStartup: new ChromeEvent(),
onMessageExternal: new ChromeEvent(),
onConnectExternal: new ChromeEvent()
onConnectExternal: new ChromeEvent(),
// see: https://developer.chrome.com/apps/runtime#method-sendMessage
sendMessage: function (message: object, responseCallback: () => void) {
const onMessage = chrome.runtime.onMessage as OnMessageEvent
onMessage.emit(message)
responseCallback()
}
},
browserAction: {
setBadgeBackgroundColor: function (properties: object) {
Expand Down Expand Up @@ -151,6 +162,12 @@ export const getMockChrome = () => {
insertCSS: function (details: jest.SpyInstance) {
return
},
query: function (queryInfo: chrome.tabs.QueryInfo, callback: (result: chrome.tabs.Tab[]) => void) {
return callback
},
sendMessage: function (tabID: Number, message: any, options: object, responseCallback: any) {
return responseCallback
},
onActivated: new ChromeEvent(),
onCreated: new ChromeEvent(),
onUpdated: new ChromeEvent()
Expand Down Expand Up @@ -222,8 +239,27 @@ export const getMockChrome = () => {
search: function (query: string, callback: (results: chrome.bookmarks.BookmarkTreeNode[]) => void) {
return
}
},
contextMenus: {
create: function (data: any) {
return Promise.resolve()
},
onBlocked: new ChromeEvent(),
allowScriptsOnce: function (origins: Array<string>, tabId: number, cb: () => void) {
setImmediate(cb)
},
onClicked: new ChromeEvent()
}
}
return mock
}
export const window = () => {
let mock = {
prompt: function (text: String) {
return text
}
}
return mock
}

export const initialState = deepFreeze({
Expand Down