aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/completion.coffee2
-rw-r--r--background_scripts/exclusions.coffee35
-rw-r--r--background_scripts/main.coffee19
-rw-r--r--lib/utils.coffee18
-rw-r--r--pages/exclusions.html13
-rw-r--r--pages/options.coffee276
-rw-r--r--pages/options.css232
-rw-r--r--pages/options.html252
-rw-r--r--pages/popup.coffee110
-rw-r--r--pages/popup.html113
-rw-r--r--tests/unit_tests/exclusion_test.coffee50
-rw-r--r--tests/unit_tests/utils_test.coffee3
12 files changed, 554 insertions, 569 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index dc5519d5..d62f82fe 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -327,7 +327,7 @@ class SearchEngineCompleter
searchEngineMatch = this.getSearchEngineMatches(queryTerms[0])
suggestions = []
if searchEngineMatch
- searchEngineMatch = searchEngineMatch.replace(/%s/g, queryTerms[1..].join(" "))
+ searchEngineMatch = searchEngineMatch.replace(/%s/g, Utils.createSearchQuery queryTerms[1..])
suggestion = new Suggestion(queryTerms, "search", searchEngineMatch, queryTerms[0] + ": " + queryTerms[1..].join(" "), @computeRelevancy)
suggestions.push(suggestion)
onComplete(suggestions)
diff --git a/background_scripts/exclusions.coffee b/background_scripts/exclusions.coffee
index 2b34238b..55ced3ef 100644
--- a/background_scripts/exclusions.coffee
+++ b/background_scripts/exclusions.coffee
@@ -15,17 +15,27 @@ RegexpCache =
# The Exclusions class manages the exclusion rule setting.
# An exclusion is an object with two attributes: pattern and passKeys.
-# The exclusions are an array of such objects (because the order matters).
+# The exclusions are an array of such objects.
root.Exclusions = Exclusions =
+ # Make RegexpCache, which is required on the page popup, accessible via the Exclusions object.
+ RegexpCache: RegexpCache
rules: Settings.get("exclusionRules")
- # Return the first exclusion rule matching the URL, or null.
- getRule: (url) ->
- for rule in @rules
- return rule if url.match(RegexpCache.get(rule.pattern))
- return null
+ # Merge the matching rules for URL, or null. In the normal case, we use the configured @rules; hence, this
+ # is the default. However, when called from the page popup, we are testing what effect candidate new rules
+ # would have on the current tab. In this case, the candidate rules are provided by the caller.
+ getRule: (url, rules=@rules) ->
+ matches = (rule for rule in rules when rule.pattern and 0 <= url.search(RegexpCache.get(rule.pattern)))
+ # An absolute exclusion rule (with no passKeys) takes priority.
+ for rule in matches
+ return rule unless rule.passKeys
+ if 0 < matches.length
+ pattern: (rule.pattern for rule in matches).join " | " # Not used; for debugging only.
+ passKeys: Utils.distinctCharacters (rule.passKeys for rule in matches).join ""
+ else
+ null
setRules: (rules) ->
# Callers map a rule to null to have it deleted, and rules without a pattern are useless.
@@ -35,19 +45,6 @@ root.Exclusions = Exclusions =
postUpdateHook: (rules) ->
@rules = rules
- # Update an existing rule or add a new rule.
- updateOrAdd: (newRule) ->
- seen = false
- @rules.push(newRule)
- @setRules @rules.map (rule) ->
- if rule.pattern == newRule.pattern
- if seen then null else seen = newRule
- else
- rule
-
- remove: (pattern) ->
- @setRules(@rules.filter((rule) -> rule and rule.pattern != pattern))
-
# Development and debug only.
# Enable this (temporarily) to restore legacy exclusion rules from backup.
if false and Settings.has("excludedUrlsBackup")
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 44ab5bac..4c1b9ae7 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -80,26 +80,10 @@ getCurrentTabUrl = (request, sender) -> sender.tab.url
root.isEnabledForUrl = isEnabledForUrl = (request) ->
rule = Exclusions.getRule(request.url)
{
- rule: rule
isEnabledForUrl: not rule or rule.passKeys
passKeys: rule?.passKeys or ""
}
-# Called by the popup UI.
-# If the URL pattern matches an existing rule, then the existing rule is updated. Otherwise, a new rule is created.
-root.addExclusionRule = (pattern,passKeys) ->
- if pattern = pattern.trim()
- Exclusions.updateOrAdd({ pattern: pattern, passKeys: passKeys })
- chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true },
- (tabs) -> updateActiveState(tabs[0].id))
-
-# Called by the popup UI. Remove all existing exclusion rules with this pattern.
-root.removeExclusionRule = (pattern) ->
- if pattern = pattern.trim()
- Exclusions.remove(pattern)
- chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true },
- (tabs) -> updateActiveState(tabs[0].id))
-
saveHelpDialogSettings = (request) ->
Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands)
@@ -358,7 +342,8 @@ setBrowserActionIcon = (tabId,path) ->
# Updates the browserAction icon to indicate whether Vimium is enabled or disabled on the current page.
# Also propagates new enabled/disabled/passkeys state to active window, if necessary.
# This lets you disable Vimium on a page without needing to reload.
-updateActiveState = (tabId) ->
+# Exported via root because it's called from the page popup.
+root.updateActiveState = updateActiveState = (tabId) ->
enabledIcon = "icons/browser_action_enabled.png"
disabledIcon = "icons/browser_action_disabled.png"
partialIcon = "icons/browser_action_partial.png"
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 2efb4716..661f7e84 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -87,11 +87,17 @@ Utils =
# Fallback: no URL
return false
+ # Map a search query to its URL encoded form. The query may be either a string or an array of strings.
+ # E.g. "BBC Sport" -> "BBC+Sport".
+ createSearchQuery: (query) ->
+ query = query.split(/\s+/) if typeof(query) == "string"
+ query.map(encodeURIComponent).join "+"
+
# Creates a search URL from the given :query.
createSearchUrl: (query) ->
- # it would be better to pull the default search engine from chrome itself,
- # but it is not clear if/how that is possible
- Settings.get("searchUrl") + encodeURIComponent(query)
+ # It would be better to pull the default search engine from chrome itself. However, unfortunately chrome
+ # does not provide an API for doing so.
+ Settings.get("searchUrl") + @createSearchQuery query
# Converts :string into a Google search if it's not already a URL. We don't bother with escaping characters
# as Chrome will do that for us.
@@ -109,6 +115,12 @@ Utils =
# detects both literals and dynamically created strings
isString: (obj) -> typeof obj == 'string' or obj instanceof String
+ # Transform "zjkjkabz" into "abjkz".
+ distinctCharacters: (str) ->
+ unique = ""
+ for char in str.split("").sort()
+ unique += char unless 0 <= unique.indexOf char
+ unique
# Compares two version strings (e.g. "1.1" and "1.5") and returns
# -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB.
diff --git a/pages/exclusions.html b/pages/exclusions.html
new file mode 100644
index 00000000..b09f2895
--- /dev/null
+++ b/pages/exclusions.html
@@ -0,0 +1,13 @@
+<table id="exclusionRules">
+ <tr>
+ <td><span class="exclusionHeaderText">Patterns</span></td>
+ <td><span class="exclusionHeaderText">Keys</span></td>
+ </tr>
+</table>
+<template id="exclusionRuleTemplate">
+ <tr class="exclusionRuleTemplateInstance">
+ <td><input/ type="text" class="pattern" spellcheck="false" placeholder="URL pattern"></td>
+ <td class="exclusionRulePassKeys"><input/ type="text" class="passKeys" spellcheck="false" placeholder="Exclude keys"></td>
+ <td class="exclusionRemoveButton"><input/ type="button" class="exclusionRemoveButtonButton" value="&#x2716;"></td>
+ </tr>
+</template>
diff --git a/pages/options.coffee b/pages/options.coffee
index cd19fa37..93c9b503 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -1,6 +1,8 @@
$ = (id) -> document.getElementById id
+bgUtils = chrome.extension.getBackgroundPage().Utils
bgSettings = chrome.extension.getBackgroundPage().Settings
+bgExclusions = chrome.extension.getBackgroundPage().Exclusions
#
# Class hierarchy for various types of option.
@@ -11,10 +13,9 @@ class Option
# Static. Array of all options.
@all = []
- constructor: (field,enableSaveButton) ->
- @field = field
+ constructor: (@field,@onUpdated) ->
@element = $(@field)
- @element.addEventListener "change", enableSaveButton
+ @element.addEventListener "change", @onUpdated
@fetch()
Option.all.push @
@@ -41,8 +42,6 @@ class Option
# Static method.
@saveOptions: ->
Option.all.map (option) -> option.save()
- $("saveOptions").disabled = true
- $("saveOptions").innerHTML = "No Changes"
# Abstract method; only implemented in sub-classes.
# Populate the option's DOM element (@element) with the setting's current value.
@@ -57,16 +56,16 @@ class NumberOption extends Option
readValueFromElement: -> parseFloat @element.value
class TextOption extends Option
- constructor: (field,enableSaveButton) ->
- super(field,enableSaveButton)
- @element.addEventListener "input", enableSaveButton
+ constructor: (args...) ->
+ super(args...)
+ @element.addEventListener "input", @onUpdated
populateElement: (value) -> @element.value = value
readValueFromElement: -> @element.value.trim()
class NonEmptyTextOption extends Option
- constructor: (field,enableSaveButton) ->
- super(field,enableSaveButton)
- @element.addEventListener "input", enableSaveButton
+ constructor: (args...) ->
+ super(args...)
+ @element.addEventListener "input", @onUpdated
populateElement: (value) -> @element.value = value
# If the new value is not empty, then return it. Otherwise, restore the default value.
@@ -80,18 +79,24 @@ class ExclusionRulesOption extends Option
constructor: (args...) ->
super(args...)
$("exclusionAddButton").addEventListener "click", (event) =>
- @appendRule { pattern: "", passKeys: "" }
- # Focus the pattern element in the new rule.
- @element.children[@element.children.length-1].children[0].children[0].focus()
- # Scroll the new rule into view.
+ @addRule()
+
+ # Add a new rule, focus its pattern, scroll it into view, and return the newly-added element. On the
+ # options page, there is no current URL, so there is no initial pattern. This is the default. On the popup
+ # page (see ExclusionRulesOnPopupOption), the pattern is pre-populated based on the current tab's URL.
+ addRule: (pattern="") ->
+ element = @appendRule { pattern: pattern, passKeys: "" }
+ @getPattern(element).focus()
exclusionScrollBox = $("exclusionScrollBox")
exclusionScrollBox.scrollTop = exclusionScrollBox.scrollHeight
+ @onUpdated()
+ element
populateElement: (rules) ->
for rule in rules
@appendRule rule
- # Append a row for a new rule.
+ # Append a row for a new rule. Return the newly-added element.
appendRule: (rule) ->
content = document.querySelector('#exclusionRuleTemplate').content
row = document.importNode content, true
@@ -100,22 +105,21 @@ class ExclusionRulesOption extends Option
element = row.querySelector ".#{field}"
element.value = rule[field]
for event in [ "input", "change" ]
- element.addEventListener event, enableSaveButton
+ element.addEventListener event, @onUpdated
- remove = row.querySelector ".exclusionRemoveButton"
- remove.addEventListener "click", (event) =>
- row = event.target.parentNode.parentNode
- row.parentNode.removeChild row
- enableSaveButton()
+ @getRemoveButton(row).addEventListener "click", (event) =>
+ rule = event.target.parentNode.parentNode
+ rule.parentNode.removeChild rule
+ @onUpdated()
@element.appendChild row
+ @element.children[@element.children.length-1]
readValueFromElement: ->
rules =
for element in @element.getElementsByClassName "exclusionRuleTemplateInstance"
- pattern = element.children[0].firstChild.value.trim()
- passKeys = element.children[1].firstChild.value.trim()
- { pattern: pattern, passKeys: passKeys }
+ pattern: @getPattern(element).value.split(/\s+/).join ""
+ passKeys: @getPassKeys(element).value.split(/\s+/).join ""
rules.filter (rule) -> rule.pattern
areEqual: (a,b) ->
@@ -124,48 +128,123 @@ class ExclusionRulesOption extends Option
flatten = (rule) -> if rule and rule.pattern then rule.pattern + "\n" + rule.passKeys else ""
a.map(flatten).join("\n") == b.map(flatten).join("\n")
-#
-# Operations for page elements.
-enableSaveButton = ->
- $("saveOptions").removeAttribute "disabled"
- $("saveOptions").innerHTML = "Save Changes"
-
-# Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints".
-maintainLinkHintsView = ->
- hide = (el) -> el.parentNode.parentNode.style.display = "none"
- show = (el) -> el.parentNode.parentNode.style.display = "table-row"
- if $("filterLinkHints").checked
- hide $("linkHintCharacters")
- show $("linkHintNumbers")
- else
- show $("linkHintCharacters")
- hide $("linkHintNumbers")
-
-toggleAdvancedOptions =
- do (advancedMode=false) ->
- (event) ->
- if advancedMode
- $("advancedOptions").style.display = "none"
- $("advancedOptionsLink").innerHTML = "Show advanced options&hellip;"
+ # Accessors for the three main sub-elements of an "exclusionRuleTemplateInstance".
+ getPattern: (element) -> element.querySelector(".pattern")
+ getPassKeys: (element) -> element.querySelector(".passKeys")
+ getRemoveButton: (element) -> element.querySelector(".exclusionRemoveButtonButton")
+
+# ExclusionRulesOnPopupOption is ExclusionRulesOption, extended with some UI tweeks suitable for use in the
+# page popup. This also differs from ExclusionRulesOption in that, on the page popup, there is always a URL
+# (@url) associated with the current tab.
+class ExclusionRulesOnPopupOption extends ExclusionRulesOption
+ constructor: (@url, args...) ->
+ super(args...)
+
+ addRule: ->
+ element = super @generateDefaultPattern()
+ @activatePatternWatcher element
+ # ExclusionRulesOption.addRule()/super() has focused the pattern. Here, focus the passKeys instead;
+ # because, in the popup, we already have a pattern, so the user is more likely to edit the passKeys.
+ @getPassKeys(element).focus()
+ # Return element (for consistency with ExclusionRulesOption.addRule()).
+ element
+
+ populateElement: (rules) ->
+ super(rules)
+ elements = @element.getElementsByClassName "exclusionRuleTemplateInstance"
+ @activatePatternWatcher element for element in elements
+
+ haveMatch = false
+ for element in elements
+ pattern = @getPattern(element).value.trim()
+ if 0 <= @url.search bgExclusions.RegexpCache.get pattern
+ haveMatch = true
+ @getPassKeys(element).focus()
+ else
+ element.style.display = 'none'
+ @addRule() unless haveMatch
+
+ # Provide visual feedback (make it red) when a pattern does not match the current tab's URL.
+ activatePatternWatcher: (element) ->
+ patternElement = element.children[0].firstChild
+ patternElement.addEventListener "keyup", =>
+ if @url.match bgExclusions.RegexpCache.get patternElement.value
+ patternElement.title = patternElement.style.color = ""
else
- $("advancedOptions").style.display = "table-row-group"
- $("advancedOptionsLink").innerHTML = "Hide advanced options"
- advancedMode = !advancedMode
- event.preventDefault()
- # Prevent the "advanced options" link from retaining the focus.
- document.activeElement.blur()
-
-activateHelpDialog = ->
- showHelpDialog chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId
- # Prevent the "show help" link from retaining the focus.
- document.activeElement.blur()
+ patternElement.style.color = "red"
+ patternElement.title = "Red text means that the pattern does not\nmatch the current URL."
+
+ # Generate a default exclusion-rule pattern from a URL. This is then used to pre-populate the pattern on
+ # the page popup.
+ generateDefaultPattern: ->
+ if /^https?:\/\/./.test @url
+ # The common use case is to disable Vimium at the domain level.
+ # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html".
+ "https?:/" + @url.split("/",3)[1..].join("/") + "/*"
+ else if /^[a-z]{3,}:\/\/./.test @url
+ # Anything else which seems to be a URL.
+ @url.split("/",3).join("/") + "/*"
+ else
+ @url + "*"
+
+initOptionsPage = ->
+ onUpdated = ->
+ $("saveOptions").removeAttribute "disabled"
+ $("saveOptions").innerHTML = "Save Changes"
+
+ # Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints".
+ maintainLinkHintsView = ->
+ hide = (el) -> el.parentNode.parentNode.style.display = "none"
+ show = (el) -> el.parentNode.parentNode.style.display = "table-row"
+ if $("filterLinkHints").checked
+ hide $("linkHintCharacters")
+ show $("linkHintNumbers")
+ else
+ show $("linkHintCharacters")
+ hide $("linkHintNumbers")
+
+ toggleAdvancedOptions =
+ do (advancedMode=false) ->
+ (event) ->
+ if advancedMode
+ $("advancedOptions").style.display = "none"
+ $("advancedOptionsLink").innerHTML = "Show advanced options&hellip;"
+ else
+ $("advancedOptions").style.display = "table-row-group"
+ $("advancedOptionsLink").innerHTML = "Hide advanced options"
+ advancedMode = !advancedMode
+ event.preventDefault()
+ # Prevent the "advanced options" link from retaining the focus.
+ document.activeElement.blur()
+
+ activateHelpDialog = ->
+ showHelpDialog chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId
+ # Prevent the "show help" link from retaining the focus when clicked.
+ document.activeElement.blur()
+
+ saveOptions = ->
+ Option.saveOptions()
+ $("saveOptions").disabled = true
+ $("saveOptions").innerHTML = "No Changes"
-#
-# Initialization.
-document.addEventListener "DOMContentLoaded", ->
+ $("saveOptions").addEventListener "click", saveOptions
+ $("advancedOptionsLink").addEventListener "click", toggleAdvancedOptions
+ $("showCommands").addEventListener "click", activateHelpDialog
+ $("filterLinkHints").addEventListener "click", maintainLinkHintsView
+
+ for element in document.getElementsByClassName "nonEmptyTextOption"
+ element.className = element.className + " example info"
+ element.innerHTML = "Leave empty to reset this option."
- # Populate options. The constructor adds each new object to "Option.all".
- new type(name,enableSaveButton) for name, type of {
+ maintainLinkHintsView()
+ window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled
+
+ document.addEventListener "keyup", (event) ->
+ if event.ctrlKey and event.keyCode == 13
+ document.activeElement.blur() if document?.activeElement?.blur
+ saveOptions()
+
+ options =
exclusionRules: ExclusionRulesOption
filterLinkHints: CheckBoxOption
hideHud: CheckBoxOption
@@ -181,22 +260,63 @@ document.addEventListener "DOMContentLoaded", ->
searchEngines: TextOption
searchUrl: NonEmptyTextOption
userDefinedLinkHintCss: TextOption
- }
- $("saveOptions").addEventListener "click", Option.saveOptions
- $("advancedOptionsLink").addEventListener "click", toggleAdvancedOptions
- $("showCommands").addEventListener "click", activateHelpDialog
- $("filterLinkHints").addEventListener "click", maintainLinkHintsView
+ # Populate options. The constructor adds each new object to "Option.all".
+ for name, type of options
+ new type(name,onUpdated)
+
+initPopupPage = ->
+ chrome.tabs.getSelected null, (tab) ->
+ exclusions = null
+ document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html")
+
+ updateState = ->
+ rule = bgExclusions.getRule tab.url, exclusions.readValueFromElement()
+ $("state").innerHTML = "Vimium will " +
+ if rule and rule.passKeys
+ "exclude <span class='code'>#{rule.passKeys}</span>"
+ else if rule
+ "be disabled"
+ else
+ "be enabled"
+
+ onUpdated = ->
+ $("helpText").innerHTML = "Type <strong>Ctrl-Enter</strong> to save and close."
+ $("saveOptions").removeAttribute "disabled"
+ $("saveOptions").innerHTML = "Save Changes"
+ updateState() if exclusions
+
+ saveOptions = ->
+ Option.saveOptions()
+ $("saveOptions").innerHTML = "Saved"
+ $("saveOptions").disabled = true
+ chrome.tabs.query { windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, (tabs) ->
+ chrome.extension.getBackgroundPage().updateActiveState(tabs[0].id)
- for element in document.getElementsByClassName "nonEmptyTextOption"
- element.className = element.className + " example info"
- element.innerHTML = "Leave empty to reset this option."
+ $("saveOptions").addEventListener "click", saveOptions
- maintainLinkHintsView()
- window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled
+ document.addEventListener "keyup", (event) ->
+ if event.ctrlKey and event.keyCode == 13
+ saveOptions()
+ window.close()
- document.addEventListener "keyup", (event) ->
- if event.ctrlKey and event.keyCode == 13
- document.activeElement.blur() if document?.activeElement?.blur
- Option.saveOptions()
+ # Populate options. Just one, here.
+ exclusions = new ExclusionRulesOnPopupOption(tab.url, "exclusionRules", onUpdated)
+
+ updateState()
+ document.addEventListener "keyup", updateState
+
+#
+# Initialization.
+document.addEventListener "DOMContentLoaded", ->
+ xhr = new XMLHttpRequest()
+ xhr.open 'GET', chrome.extension.getURL('pages/exclusions.html'), true
+ xhr.onreadystatechange = ->
+ if xhr.readyState == 4
+ $("exclusionScrollBox").innerHTML = xhr.responseText
+ switch location.pathname
+ when "/pages/options.html" then initOptionsPage()
+ when "/pages/popup.html" then initPopupPage()
+
+ xhr.send()
diff --git a/pages/options.css b/pages/options.css
new file mode 100644
index 00000000..5b098c8f
--- /dev/null
+++ b/pages/options.css
@@ -0,0 +1,232 @@
+/* NOTE: This stylesheet is included in both options.html and popup.html. So changes here affect
+ both of these. */
+body {
+ font: 14px "DejaVu Sans", "Arial", sans-serif;
+ color: #303942;
+ margin: 0 auto;
+}
+a, a:visited { color: #15c; }
+a:active { color: #052577; }
+div#wrapper, #footerWrapper {
+ width: 540px;
+ margin-left: 35px;
+}
+header {
+ font-size: 18px;
+ font-weight: normal;
+ border-bottom: 1px solid #eee;
+ padding: 20px 0 15px 0;
+ width: 100%;
+}
+button {
+ -webkit-user-select: none;
+ -webkit-appearance: none;
+ background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
+ border: 1px solid rgba(0, 0, 0, 0.25);
+ border-radius: 2px;
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75);
+ color: #444;
+ font: inherit;
+ text-shadow: 0 1px 0 #f0f0f0;
+ height: 24px;
+ font-size: 12px;
+ padding: 0 10px;
+}
+button:hover {
+ background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
+ border-color: rgba(0, 0, 0, 0.3);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95);
+ color: black;
+}
+button:active {
+ background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
+ box-shadow: none;
+ text-shadow: none;
+}
+button[disabled], button[disabled]:hover, button[disabled]:active {
+ background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
+ border: 1px solid rgba(0, 0, 0, 0.25);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75);
+ text-shadow: 0 1px 0 #f0f0f0;
+ color: #888;
+}
+input[type="checkbox"] {
+ -webkit-user-select: none;
+}
+label:hover {
+ color: black;
+}
+pre, code, .code {
+ font-family: Consolas, "Liberation Mono", Courier, monospace;
+}
+pre {
+ margin: 5px;
+ border-left: 1px solid #eee;
+ padding-left: 5px;
+
+}
+input, textarea {
+ box-sizing: border-box;
+}
+textarea {
+ /* Horizontal resizing is pretty screwy-looking. */
+ resize: vertical;
+}
+table#options{
+ width: 100%;
+ font-size: 14px;
+ position: relative;
+ border-spacing: 0 23px;
+}
+.example {
+ font-size: 12px;
+ line-height: 16px;
+ color: #979ca0;
+ margin-left: 20px;
+}
+.info {
+ margin-left: 0px;
+}
+.caption {
+ margin-right: 10px;
+ min-width: 130px;
+ padding-top: 3px;
+ vertical-align: top;
+}
+td { padding: 0; }
+div#exampleKeyMapping {
+ margin-left: 10px;
+ margin-top: 5px;
+}
+input#linkHintCharacters {
+ width: 100%;
+}
+input#linkHintNumbers {
+ width: 100%;
+}
+input#linkHintCharacters {
+ width: 100%;
+}
+input#scrollStepSize {
+ width: 40px;
+ margin-right: 3px;
+}
+textarea#userDefinedLinkHintCss, textarea#keyMappings, textarea#searchEngines {
+ width: 100%;;
+ min-height: 130px;
+ white-space: nowrap;
+}
+input#previousPatterns, input#nextPatterns {
+ width: 100%;
+}
+input#newTabUrl {
+ width: 100%;
+}
+input#searchUrl {
+ width: 100%;
+}
+#status {
+ margin-left: 10px;
+ font-size: 80%;
+}
+/* Make the caption in the settings table as small as possible, to pull the other fields to the right. */
+.caption {
+ width: 1px;
+ white-space: nowrap;
+}
+#buttonsPanel { width: 100%; }
+#advancedOptions { display: none; }
+#advancedOptionsLink { line-height: 24px; }
+.help {
+ position: absolute;
+ right: -320px;
+ width: 320px;
+}
+input:read-only {
+ background-color: #eee;
+ color: #666;
+ pointer-events: none;
+ -webkit-user-select: none;
+}
+input[type="text"], textarea {
+ border: 1px solid #bfbfbf;
+ border-radius: 2px;
+ color: #444;
+ font: inherit;
+ padding: 3px;
+}
+button:focus, input[type="text"]:focus, textarea:focus {
+ -webkit-transition: border-color 200ms;
+ border-color: #4d90fe;
+ outline: none;
+}
+/* Boolean options have a tighter form representation than text options. */
+td.booleanOption { font-size: 12px; }
+/* Ids and classes for rendering exclusionRules */
+#exclusionScrollBox {
+ overflow: scroll;
+ overflow-x: hidden;
+ overflow-y: auto;
+ /* Each exclusion rule is about 30px, so this allows 7 before scrolling */
+ max-height: 215px;
+ min-height: 75px;
+ border-radius: 2px;
+ color: #444;
+ width: 100%
+}
+#exclusionRules {
+ width: 100%;
+}
+.exclusionRulePassKeys {
+ width: 33%;
+}
+.exclusionRemoveButton {
+ width: 1px; /* 1px; smaller than the button itself. */
+}
+.exclusionRemoveButtonButton {
+ border: none;
+ background-color: #fff;
+ color: #979ca0;
+}
+.exclusionRemoveButtonButton:hover {
+ color: #444;
+}
+input.pattern, input.passKeys, .exclusionHeaderText {
+ width: 100%;
+ font-family: Consolas, "Liberation Mono", Courier, monospace;
+ font-size: 14px;
+}
+.exclusionHeaderText {
+ padding-left: 3px;
+ color: #979ca0;
+}
+#exclusionAddButton {
+ float: right;
+ margin-right: 0px;
+ margin-top: 5px;
+}
+#footer {
+ background: #f5f5f5;
+ border-top: 1px solid #979ca0;
+ position: fixed;
+ bottom: 0px;
+ z-index: 10;
+}
+#footer, #footerTable, #footerTableData {
+ width: 100%;
+}
+#endSpace {
+ /* Leave space for the fixed footer. */
+ min-height: 30px;
+ max-height: 30px;
+}
+#helpText {
+ font-size: 12px;
+}
+#saveOptionsTableData {
+ float: right;
+}
+#saveOptions {
+ white-space: nowrap;
+ width: 110px;
+}
diff --git a/pages/options.html b/pages/options.html
index 8e685304..ae45537a 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -1,242 +1,9 @@
<html>
<head>
<title>Vimium Options</title>
+ <link rel="stylesheet" type="text/css" href="options.css">
<script src="content_script_loader.js"></script>
- <style type="text/css" media="screen">
- body {
- font: 14px "DejaVu Sans", "Arial", sans-serif;
- color: #303942;
- margin: 0 auto;
- }
- a, a:visited { color: #15c; }
- a:active { color: #052577; }
- div#wrapper, #footerWrapper {
- width: 540px;
- margin-left: 35px;
- }
- header {
- font-size: 18px;
- font-weight: normal;
- border-bottom: 1px solid #eee;
- padding: 20px 0 15px 0;
- width: 100%;
- }
- button {
- -webkit-user-select: none;
- -webkit-appearance: none;
- background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
- border: 1px solid rgba(0, 0, 0, 0.25);
- border-radius: 2px;
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75);
- color: #444;
- font: inherit;
- text-shadow: 0 1px 0 #f0f0f0;
- height: 24px;
- font-size: 12px;
- padding: 0 10px;
- }
- button:hover {
- background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
- border-color: rgba(0, 0, 0, 0.3);
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95);
- color: black;
- }
- button:active {
- background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
- box-shadow: none;
- text-shadow: none;
- }
- button[disabled], button[disabled]:hover, button[disabled]:active {
- background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
- border: 1px solid rgba(0, 0, 0, 0.25);
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75);
- text-shadow: 0 1px 0 #f0f0f0;
- color: #888;
- }
- input[type="checkbox"] {
- -webkit-user-select: none;
- }
- label:hover {
- color: black;
- }
- pre, code, .code {
- font-family: Consolas, "Liberation Mono", Courier, monospace;
- }
- pre {
- margin: 5px;
- border-left: 1px solid #eee;
- padding-left: 5px;
-
- }
- input, textarea {
- box-sizing: border-box;
- }
- textarea {
- /* Horizontal resizing is pretty screwy-looking. */
- resize: vertical;
- }
- table#options{
- width: 100%;
- font-size: 14px;
- position: relative;
- border-spacing: 0 23px;
- }
- .example {
- font-size: 12px;
- line-height: 16px;
- color: #979ca0;
- margin-left: 20px;
- }
- .info {
- margin-left: 0px;
- }
- .caption {
- margin-right: 10px;
- min-width: 130px;
- padding-top: 3px;
- vertical-align: top;
- }
- td { padding: 0; }
- div#exampleKeyMapping {
- margin-left: 10px;
- margin-top: 5px;
- }
- input#linkHintCharacters {
- width: 100%;
- }
- input#linkHintNumbers {
- width: 100%;
- }
- input#linkHintCharacters {
- width: 100%;
- }
- input#scrollStepSize {
- width: 40px;
- margin-right: 3px;
- }
- textarea#userDefinedLinkHintCss, textarea#keyMappings, textarea#searchEngines {
- width: 100%;;
- min-height: 130px;
- white-space: nowrap;
- }
- input#previousPatterns, input#nextPatterns {
- width: 100%;
- }
- input#newTabUrl {
- width: 100%;
- }
- input#searchUrl {
- width: 100%;
- }
- #status {
- margin-left: 10px;
- font-size: 80%;
- }
- /* Make the caption in the settings table as small as possible, to pull the other fields to the right. */
- .caption {
- width: 1px;
- white-space: nowrap;
- }
- #buttonsPanel { width: 100%; }
- #advancedOptions { display: none; }
- #advancedOptionsLink { line-height: 24px; }
- .help {
- position: absolute;
- right: -320px;
- width: 320px;
- }
- input:read-only {
- background-color: #eee;
- color: #666;
- pointer-events: none;
- -webkit-user-select: none;
- }
- input[type="text"], textarea {
- border: 1px solid #bfbfbf;
- border-radius: 2px;
- color: #444;
- font: inherit;
- padding: 3px;
- }
- button:focus, input[type="text"]:focus, textarea:focus {
- -webkit-transition: border-color 200ms;
- border-color: #4d90fe;
- outline: none;
- }
- /* Boolean options have a tighter form representation than text options. */
- td.booleanOption { font-size: 12px; }
- /* Ids and classes for rendering exclusionRules */
- #exclusionScrollBox {
- overflow: scroll;
- overflow-x: hidden;
- overflow-y: auto;
- /* Each exclusion rule is about 30px, so this allows 7 before scrolling */
- max-height: 215px;
- min-height: 75px;
- border-radius: 2px;
- color: #444;
- width: 100%
- }
- #exclusionRules {
- width: 100%;
- }
- .exclusionRulePassKeys {
- width: 33%;
- }
- .exclusionRemoveButton {
- width: 1px; /* 1px; smaller than the button itself. */
- }
- .exclusionRemoveButtonButton {
- border: none;
- background-color: #fff;
- color: #979ca0;
- }
- .exclusionRemoveButtonButton:hover {
- color: #444;
- }
- input.pattern, input.passKeys, .exclusionHeaderText {
- width: 100%;
- font-family: Consolas, "Liberation Mono", Courier, monospace;
- font-size: 14px;
- }
- .exclusionHeaderText {
- padding-left: 3px;
- color: #979ca0;
- }
- #exclusionAddButton {
- float: right;
- margin-right: 0px;
- margin-top: 5px;
- }
- #footer {
- background: #f5f5f5;
- border-top: 1px solid #979ca0;
- position: fixed;
- bottom: 0px;
- z-index: 10;
- }
- #footer, #footerTable, #footerTableData {
- width: 100%;
- }
- #endSpace {
- /* Leave space for the fixed footer. */
- min-height: 30px;
- max-height: 30px;
- }
- #helpText {
- font-size: 12px;
- }
- #saveOptionsTableData {
- float: right;
- }
- #saveOptions {
- white-space: nowrap;
- width: 110px;
- }
- </style>
-
- <script type="text/javascript" src="options.js"></script>
-
+ <script type="text/javascript" src="options.js"></script>
</head>
<body>
@@ -257,20 +24,7 @@
</div>
<div>
<div id="exclusionScrollBox">
- <table id="exclusionRules">
- <tr>
- <td><span class="exclusionHeaderText">Patterns</span></td>
- <td><span class="exclusionHeaderText">Keys</span></td>
- </tr>
- </table>
- <template id="exclusionRuleTemplate">
- <tr class="exclusionRuleTemplateInstance">
- <td><input/ type="text" class="pattern" placeholder="URL pattern"></td>
- <td class="exclusionRulePassKeys"><input/ type="text" class="passKeys" placeholder="Exclude keys"></td>
- <td class="exclusionRemoveButton">
- <input/ type="button" tabindex = "-1" class="exclusionRemoveButtonButton" value="&#x2716;"></td>
- </tr>
- </template>
+ <!-- Populated from exclusions.html by options.coffee. -->
</div>
<button id="exclusionAddButton">Add Rule</button>
</div>
diff --git a/pages/popup.coffee b/pages/popup.coffee
deleted file mode 100644
index 99a4eb87..00000000
--- a/pages/popup.coffee
+++ /dev/null
@@ -1,110 +0,0 @@
-
-originalRule = undefined
-originalPattern = undefined
-originalPassKeys = undefined
-
-generateDefaultPattern = (url) ->
- if /^https?:\/\/./.test url
- # The common use case is to disable Vimium at the domain level.
- # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html".
- "https?:/" + url.split("/",3)[1..].join("/") + "/*"
- else if /^[a-z]{3,}:\/\/./.test url
- # Anything else which seems to be a URL.
- url.split("/",3).join("/") + "/*"
- else
- url + "*"
-
-reset = (initialize=false) ->
- document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html")
- chrome.tabs.getSelected null, (tab) ->
- isEnabled = chrome.extension.getBackgroundPage().isEnabledForUrl(url: tab.url)
- # Check if we have an existing exclusing rule for this page.
- if isEnabled.rule
- originalRule = isEnabled.rule
- originalPattern = originalRule.pattern
- originalPassKeys = originalRule.passKeys
- else
- originalRule = null
- originalPattern = generateDefaultPattern tab.url
- originalPassKeys = ""
- patternElement = document.getElementById("popupPattern")
- passKeysElement = document.getElementById("popupPassKeys")
- patternElement.value = originalPattern
- passKeysElement.value = originalPassKeys
- if initialize
- # Activate <Ctrl-Enter> to save.
- for element in [ patternElement, passKeysElement ]
- element.addEventListener "keyup", (event) ->
- if event.ctrlKey and event.keyCode == 13
- addExclusionRule()
- window.close()
- element.addEventListener "focus", -> document.getElementById("helpText").style.display = "block"
- element.addEventListener "blur", -> document.getElementById("helpText").style.display = "none"
- # Focus passkeys with cursor at the end (but only when creating popup).
- passKeysElement.focus()
- passKeysElement.setSelectionRange(passKeysElement.value.length, passKeysElement.value.length)
- onChange()
-
-onChange = ->
- # As the text in the popup's input elements is changed, update the the popup's buttons accordingly.
- # Aditionally, enable and disable those buttons as appropriate.
- pattern = document.getElementById("popupPattern").value.trim()
- passKeys = document.getElementById("popupPassKeys").value.trim()
- popupExclude = document.getElementById("popupExclude")
-
- document.getElementById("popupRemove").disabled =
- not (originalRule and pattern == originalPattern)
-
- if originalRule and pattern == originalPattern and passKeys == originalPassKeys
- popupExclude.disabled = true
- popupExclude.value = "Update Rule"
-
- else if originalRule and pattern == originalPattern
- popupExclude.disabled = false
- popupExclude.value = "Update Rule"
-
- else if originalRule
- popupExclude.disabled = false
- popupExclude.value = "Add Rule"
-
- else if pattern
- popupExclude.disabled = false
- popupExclude.value = "Add Rule"
-
- else
- popupExclude.disabled = true
- popupExclude.value = "Add Rule"
-
-showMessage = do ->
- timer = null
-
- hideConfirmationMessage = ->
- document.getElementById("confirmationMessage").setAttribute "style", "display: none"
- timer = null
-
- (message) ->
- document.getElementById("confirmationMessage").setAttribute "style", "display: inline-block"
- document.getElementById("confirmationMessage").innerHTML = message
- clearTimeout(timer) if timer
- timer = setTimeout(hideConfirmationMessage,2000)
-
-addExclusionRule = ->
- pattern = document.getElementById("popupPattern").value.trim()
- passKeys = document.getElementById("popupPassKeys").value.trim()
- chrome.extension.getBackgroundPage().addExclusionRule pattern, passKeys
- showMessage("Updated.")
- reset()
-
-removeExclusionRule = ->
- pattern = document.getElementById("popupPattern").value.trim()
- chrome.extension.getBackgroundPage().removeExclusionRule pattern
- showMessage("Removed.")
- reset()
-
-document.addEventListener "DOMContentLoaded", ->
- document.getElementById("popupExclude").addEventListener "click", addExclusionRule, false
- document.getElementById("popupRemove").addEventListener "click", removeExclusionRule, false
- for field in ["popupPattern", "popupPassKeys"]
- for event in ["input", "change"]
- document.getElementById(field).addEventListener event, onChange, false
- reset true
diff --git a/pages/popup.html b/pages/popup.html
index 775d6c07..c7e2fd6f 100644
--- a/pages/popup.html
+++ b/pages/popup.html
@@ -1,85 +1,82 @@
<html>
<head>
+ <link rel="stylesheet" type="text/css" href="options.css">
<style>
* {
margin: 0px;
padding: 0px;
}
- #vimiumPopup { width: 400px; }
-
- #excludeControls {
- padding: 10px;
+ #helpText, #optionsLink, #state {
+ font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif;
+ font-size: 12px;
}
- #popupPattern, #popupPassKeys {
- margin: 5px;
- width: 330px;
- /* Match the corresponding font and font size used on the options page. */
- /* TODO (smblott): Match other styles from the options page. */
- font-family: Consolas, "Liberation Mono", Courier, monospace;
- font-size: 14px;
+ #helpText, #stateLine, #state { color: #979ca0; }
+ #exclusionAddButton { width: 80px; }
+
+ #saveOptions {
+ margin-top: 5px; /* Match #exclusionAddButton */
+ margin-left: 5px;
+ float: right;
}
- #confirmationMessage {
- display: inline-block;
- width: 18px;
- height: 13px;
- background: url(icons/check.png) 3px 2px no-repeat;
- display: none;
+ #state {
+ padding-left: 5px;
+ background: #f5f5f5;
+ width: 100%;
+ border-bottom: 1px solid #979ca0;
+ margin: 0px;
}
- #popupRemove { margin: 5px; }
- #popupExclude { margin: 5px; }
+ /* These are overridden from options.css. */
- #popupMenu ul {
- list-style: none;
- }
+ #endSpace, #footerWrapper { width: 500px; }
+ #footerWrapper { margin-left: 0px; }
- #popupMenu li, #popupMenu a:active, #popupMenu a:visited {
- color: #3F6EC2;
- display: block;
- border-top: 1px solid #DDDDDD;
- padding: 3px;
- padding-left: 10px;
+ /* Make exclusionScrollBox smaller than on the options page, because there are likely to be fewer
+ matching rules, and the popup obscures the underlying page.
+ */
+ #exclusionScrollBox {
+ max-height: 124px;
+ min-height: 124px;
}
- #popupMenu a:hover {
- background: #EEEEEE;
+ #endSpace { /* Leave space for the fixed footer. */
+ min-height: 40px;
+ max-height: 40px;
}
- #optionsLink {
- font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif;
- font-size: 12px;
- float: right;
- }
- #helpText {
- color: #979ca0;
- font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif;
- font-size: 12px;
- float: left;
- display: none;
- }
</style>
- <script src="popup.js"></script>
+ <script src="options.js"></script>
</head>
<body>
- <div id="vimiumPopup">
- <div id="excludeControls">
- <input id="popupPattern" placeholder="Pattern against which to match URLs..." type="text" /><br/>
- <input id="popupPassKeys" placeholder="Only exclude these keys..." type="text" /><br/>
- <input id="popupRemove" type="button" value="Remove Rule" />
- <input id="popupExclude" type="button" value="Add or Update Rule" />
- <span id="confirmationMessage">Text is added in popup.coffee.</span>
- </div>
+ <div id="state"></div>
+
+ <div id="exclusionScrollBox">
+ <!-- Populated from exclusions.html by options.coffee. -->
+ </div>
+
+ <!-- Some extra space which is hidden underneath the footer. -->
+ <div id="endSpace"/>
- <div id="popupMenu">
- <ul>
- <li>
- <span id="helpText">Type <strong>Ctrl-Enter</strong> to save and close.</span>
- <a id="optionsLink" target="_blank">Options</a>
- </li>
- </ul>
+ <div id="footer">
+ <div id="footerWrapper">
+ <table>
+ <tr>
+ <td id="stateLine" style="width: 99%">
+ <span id="helpText">These are the rules matching this page.</span>
+ <br/>
+ <a id="optionsLink" target="_blank" tabindex="99999">Options</a>
+ </td>
+ <td valign="top">
+ <button id="exclusionAddButton">Add Rule</button>
+ </td>
+ <td valign="top">
+ <button id="saveOptions" disabled="true">No Changes</button>
+ </td>
+ </tr>
+ </table>
</div>
</div>
</body>
diff --git a/tests/unit_tests/exclusion_test.coffee b/tests/unit_tests/exclusion_test.coffee
index 25bd8125..b3ed7194 100644
--- a/tests/unit_tests/exclusion_test.coffee
+++ b/tests/unit_tests/exclusion_test.coffee
@@ -25,49 +25,33 @@ extend(global, require "../../background_scripts/main.js")
#
context "Excluded URLs and pass keys",
- # These tests have no setup, they use the default values from settings.coffee.
+ setup ->
+ Exclusions.postUpdateHook(
+ [
+ { pattern: "http*://mail.google.com/*", passKeys: "" }
+ { pattern: "http*://www.facebook.com/*", passKeys: "abab" }
+ { pattern: "http*://www.facebook.com/*", passKeys: "cdcd" }
+ { pattern: "http*://www.bbc.com/*", passKeys: "" }
+ { pattern: "http*://www.bbc.com/*", passKeys: "ab" }
+ ])
should "be disabled for excluded sites", ->
- rule = isEnabledForUrl({ url: 'http://www.google.com/calendar/page' })
- assert.isFalse rule.isEnableForUrl
+ rule = isEnabledForUrl({ url: 'http://mail.google.com/calendar/page' })
+ assert.isFalse rule.isEnabledForUrl
assert.isFalse rule.passKeys
- should "be enabled, but with pass keys", ->
- rule = isEnabledForUrl({ url: 'https://www.facebook.com/something' })
- assert.isTrue rule.isEnabledForUrl
+ should "be disabled for excluded sites, one exclusion", ->
+ rule = isEnabledForUrl({ url: 'http://www.bbc.com/calendar/page' })
+ assert.isFalse rule.isEnabledForUrl
assert.isFalse rule.passKeys
- addExclusionRule("http*://www.facebook.com/*","oO")
+
+ should "be enabled, but with pass keys", ->
rule = isEnabledForUrl({ url: 'https://www.facebook.com/something' })
assert.isTrue rule.isEnabledForUrl
- assert.equal rule.passKeys, 'oO'
+ assert.equal rule.passKeys, 'abcd'
should "be enabled", ->
rule = isEnabledForUrl({ url: 'http://www.twitter.com/pages' })
assert.isTrue rule.isEnabledForUrl
assert.isFalse rule.passKeys
- should "add a new excluded URL", ->
- rule = isEnabledForUrl({ url: 'http://www.example.com/page' })
- assert.isTrue rule.isEnabledForUrl
- addExclusionRule("http://www.example.com*")
- rule = isEnabledForUrl({ url: 'http://www.example.com/page' })
- assert.isFalse rule.isEnabledForUrl
- assert.isFalse rule.passKeys
-
- should "add a new excluded URL with passkeys", ->
- rule = isEnabledForUrl({ url: 'http://www.anotherexample.com/page' })
- assert.isTrue rule.isEnabledForUrl
- addExclusionRule("http://www.anotherexample.com/*","jk")
- rule = isEnabledForUrl({ url: 'http://www.anotherexample.com/page' })
- assert.isTrue rule.isEnabledForUrl
- assert.equal rule.passKeys, 'jk'
-
- should "update an existing excluded URL with passkeys", ->
- rule = isEnabledForUrl({ url: 'http://mail.google.com/page' })
- assert.isFalse rule.isEnabledForUrl
- assert.isFalse rule.passKeys
- addExclusionRule("http*://mail.google.com/*","jknp")
- rule = isEnabledForUrl({ url: 'http://mail.google.com/page' })
- assert.isTrue rule.isEnabledForUrl
- assert.equal rule.passKeys, 'jknp'
-
diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee
index 556f5b7a..88e9a15b 100644
--- a/tests/unit_tests/utils_test.coffee
+++ b/tests/unit_tests/utils_test.coffee
@@ -45,7 +45,8 @@ context "convertToUrl",
should "convert non-URL terms into search queries", ->
assert.equal "http://www.google.com/search?q=google", Utils.convertToUrl("google")
- assert.equal "http://www.google.com/search?q=go%20ogle.com", Utils.convertToUrl("go ogle.com")
+ assert.equal "http://www.google.com/search?q=go+ogle.com", Utils.convertToUrl("go ogle.com")
+ assert.equal "http://www.google.com/search?q=%40twitter", Utils.convertToUrl("@twitter")
context "hasChromePrefix",
should "detect chrome prefixes of URLs", ->