diff options
| -rw-r--r-- | background_scripts/main.coffee | 2 | ||||
| -rw-r--r-- | background_scripts/settings.coffee | 13 | ||||
| -rw-r--r-- | background_scripts/sync.coffee | 50 | ||||
| -rw-r--r-- | pages/options.coffee | 2 | ||||
| -rw-r--r-- | tests/unit_tests/settings_test.coffee | 45 | ||||
| -rw-r--r-- | tests/unit_tests/test_chrome_stubs.coffee | 114 | 
6 files changed, 97 insertions, 129 deletions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index dc853803..5147aec2 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -597,5 +597,5 @@ chrome.windows.getAll { populate: true }, (windows) ->          (response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response?        chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler()) -# Start pulling changes from synchrized storage. +# Start pulling changes from synchronized storage.  Sync.init() diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index f75c1db3..73a7a04b 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -23,10 +23,9 @@ root.Settings = Settings =    has: (key) -> key of localStorage -  # postUpdateHooks are called each time an option changes: -  #   either from options/options.coffee          (when the options page is saved) -  #       or from background_scripts/sync.coffee  (when an update propagates from chrome.storage) -  # +  # for settings which require action when their value changes, add hooks here +  # called from options/options.coffee (when the options page is saved), and from background_scripts/sync.coffee (when +  # an update propagates from chrome.storage.sync).    postUpdateHooks:      keyMappings: (value) ->        root.Commands.clearKeyMappingsAndSetDefaults() @@ -34,10 +33,8 @@ root.Settings = Settings =        root.refreshCompletionKeysAfterMappingSave()    # postUpdateHooks convenience wrapper -  doPostUpdateHook: (key, value) -> -    if @postUpdateHooks[key] -      @postUpdateHooks[key] value - +  performPostUpdateHook: (key, value) -> +    @postUpdateHooks[key] value if @postUpdateHooks[key]    # options/options.(coffee|html) only handle booleans and strings; therefore    # all defaults must be booleans or strings diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee index 1c6c7fb6..56c74b81 100644 --- a/background_scripts/sync.coffee +++ b/background_scripts/sync.coffee @@ -1,12 +1,12 @@  #  # * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync. -# * Sync.listener() listens for changes to chrome.storage.sync and propagates those +# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those  #   changes to localStorage and into vimium's internal state. -# * Sync.pull() polls chrome.storage.sync at startup, similarly propagating +# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating  #   changes to localStorage and into vimium's internal state.  #  # Changes are propagated into vimium's state using the same mechanism -# (Settings.doPostUpdateHook) that is used when options are changed on +# (Settings.performPostUpdateHook) that is used when options are changed on  # the options page.  #  # The effect is best-effort synchronization of vimium options/settings between @@ -26,40 +26,38 @@ root.Sync = Sync =    # However, if users have problems, they are unlikely to notice and make sense of console logs on    # background pages.  So disable it, by default.    # For genuine errors, we call console.log directly. -  debug: false +  debug: true    storage: chrome.storage.sync    doNotSync: [ "settingsVersion", "previousVersion" ] -  # This is called in main(). +  # This is called in main.coffee.    init: -> -    chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area -    @pull() +    chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area +    @fetchAsync()    # Asynchronous fetch from synced storage, called only at startup. -  pull: -> +  fetchAsync: ->      @storage.get null, (items) =>        # Chrome sets chrome.runtime.lastError if there is an error.        if chrome.runtime.lastError is undefined          for own key, value of items -          @log "pull: #{key} <- #{value}" +          @log "fetchAsync: #{key} <- #{value}"            @storeAndPropagate key, value        else -        console.log "callback for Sync.pull() indicates error" +        console.log "callback for Sync.fetchAsync() indicates error"          console.log chrome.runtime.lastError    # Asynchronous message from synced storage. -  listener: (changes, area) -> +  handleStorageUpdate: (changes, area) ->      for own key, change of changes -      @log "listener: #{key} <- #{change.newValue}" +      @log "handleStorageUpdate: #{key} <- #{change.newValue}"        @storeAndPropagate key, change?.newValue -  # Only ever called from asynchronous synced-storage callbacks (pull and listener). +  # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).    storeAndPropagate: (key, value) ->      return if not key of Settings.defaults -    return if not @isSyncKey key +    return if not @shouldSyncKey key      return if value and key of localStorage and localStorage[key] is value - -    # Ok: store and propagate this update.      defaultValue = Settings.defaults[key]      defaultValueJSON = JSON.stringify(defaultValue) @@ -67,20 +65,22 @@ root.Sync = Sync =        # Key/value has been changed to non-default value at remote instance.        @log "storeAndPropagate update: #{key}=#{value}"        localStorage[key] = value -      Settings.doPostUpdateHook key, JSON.parse(value) +      Settings.performPostUpdateHook key, JSON.parse(value)      else        # Key has been reset to default value at remote instance.        @log "storeAndPropagate clear: #{key}"        if key of localStorage          delete localStorage[key] -      Settings.doPostUpdateHook key, defaultValue +      Settings.performPostUpdateHook key, defaultValue    # Only called synchronously from within vimium, never on a callback.    # No need to propagate updates to the rest of vimium, that's already been done.    set: (key, value) -> -    if @isSyncKey key +    if @shouldSyncKey key        @log "set scheduled: #{key}=#{value}" -      @storage.set @mkKeyValue(key,value), => +      key_value = {} +      key_value[key] = value +      @storage.set key_value, =>          # Chrome sets chrome.runtime.lastError if there is an error.          if chrome.runtime.lastError            console.log "callback for Sync.set() indicates error: #{key} <- #{value}" @@ -88,7 +88,7 @@ root.Sync = Sync =    # Only called synchronously from within vimium, never on a callback.    clear: (key) -> -    if @isSyncKey key +    if @shouldSyncKey key        @log "clear scheduled: #{key}"        @storage.remove key, =>          # Chrome sets chrome.runtime.lastError if there is an error. @@ -97,15 +97,9 @@ root.Sync = Sync =            console.log chrome.runtime.lastError    # Should we synchronize this key? -  isSyncKey: (key) -> +  shouldSyncKey: (key) ->      key not in @doNotSync -  # There has to be a more elegant way to do this! -  mkKeyValue: (key, value) -> -    obj = {} -    obj[key] = value -    obj -    log: (msg) ->      console.log "Sync: #{msg}" if @debug diff --git a/pages/options.coffee b/pages/options.coffee index 0aeb8e2d..34696f68 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -67,7 +67,7 @@ saveOptions = ->        bgSettings.set fieldName, fieldValue      $(fieldName).value = fieldValue      $(fieldName).setAttribute "savedValue", fieldValue -    bgSettings.doPostUpdateHook fieldName, fieldValue +    bgSettings.performPostUpdateHook fieldName, fieldValue    $("saveOptions").disabled = true diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index dd2815f0..25bb3628 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -13,7 +13,7 @@ context "settings",    setup ->      stub global, 'localStorage', {} -  should "all settings stored in localStorage must be JSONified strings", -> +  should "save settings in localStorage as JSONified strings", ->      Settings.set 'dummy', ""      assert.equal localStorage.dummy, '""' @@ -36,58 +36,39 @@ context "settings",      Settings.clear 'scrollStepSize'      assert.equal Settings.get('scrollStepSize'), 60 -  should "remote changes take effect locally, non-default value", -> +  should "propagate non-default value via synced storage listener", ->      Settings.set 'scrollStepSize', 20      assert.equal Settings.get('scrollStepSize'), 20 -    Sync.listener { scrollStepSize: { newValue: "40" } } +    Sync.handleStorageUpdate { scrollStepSize: { newValue: "40" } }      assert.equal Settings.get('scrollStepSize'), 40 -  should "remote changes take effect locally, default value", -> +  should "propagate default value via synced storage listener", ->      Settings.set 'scrollStepSize', 20      assert.equal Settings.get('scrollStepSize'), 20 -    Sync.listener { scrollStepSize: { newValue: "60" } } +    Sync.handleStorageUpdate { scrollStepSize: { newValue: "60" } }      assert.isFalse Settings.has 'scrollStepSize' -  should "remote changes are propagated, non-default value", -> -    # Prime Sync. -    Settings.set 'scrollStepSize', 20 -    assert.equal Settings.get('scrollStepSize'), 20 -    # Set a bogus value in localStorage, bypassing Settings and Sync. -    localStorage['scrollStepSize'] = JSON.stringify(10) -    assert.equal Settings.get('scrollStepSize'), 10 -    # Pull Sync's version of scrollStepSize, this should reset it to the correct value (20). -    Sync.pull() +  should "propagate non-default values from synced storage", -> +    chrome.storage.sync.set { scrollStepSize: JSON.stringify(20) } +    Sync.fetchAsync()      assert.equal Settings.get('scrollStepSize'), 20 -  should "remote changes are propagated, default value", -> -    # Prime Sync with a default value. +  should "propagate default values from synced storage", -> +    Settings.set 'scrollStepSize', 20      chrome.storage.sync.set { scrollStepSize: JSON.stringify(60) } -    assert.isFalse Settings.has 'scrollStepSize' -    # Set a bogus value in localStorage, bypassing Settings and Sync. -    localStorage['scrollStepSize'] = JSON.stringify(10) -    assert.equal Settings.get('scrollStepSize'), 10 -    # Pull Sync's version of scrollStepSize, this should delete scrollStepSize in localStorage, because it's a default value. -    Sync.pull() +    Sync.fetchAsync()      assert.isFalse Settings.has 'scrollStepSize' -  should "remote setting cleared", -> -    # Prime localStorage. +  should "clear a setting from synced storage", ->      Settings.set 'scrollStepSize', 20 -    assert.equal Settings.get('scrollStepSize'), 20 -    # Prime Sync with a non-default value. -    chrome.storage.sync.set { scrollStepSize: JSON.stringify(40) }      chrome.storage.sync.remove 'scrollStepSize'      assert.isFalse Settings.has 'scrollStepSize'    should "trigger a postUpdateHook", ->      message = "Hello World" -    # Install a bogus update hook for an existing setting.      Settings.postUpdateHooks['scrollStepSize'] = (value) -> Sync.message = value      chrome.storage.sync.set { scrollStepSize: JSON.stringify(message) } -    # Was the update hook triggered?      assert.equal message, Sync.message -  should "sync a key which is not a setting", -> -    # There is nothing to test, here.  It's purpose is just to ensure that, should additional settings be -    # added in future, then the Sync won't cause unexpected crashes. +  should "sync a key which is not a known setting (without crashing)", ->      chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") } diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index dd8dccf8..e9c48f31 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -1,65 +1,61 @@ -global.chrome ||= {} -global.runtime ||= {} -global.chrome.storage ||= {}  #  # This is a stub for chrome.strorage.sync for testing.  # It does what chrome.storage.sync should do (roughly), but does so synchronously.  # -global.chrome.storage.onChanged ||= -  addListener: (func) -> @func = func - -  # Fake a callback from chrome.storage.sync. -  call: (key,value) -> -    chrome.runtime = { lastError: undefined } -    if @func -      @func( @mkKeyValue(key,value), 'synced storage stub' ) - -  callEmpty: (key) -> -    chrome.runtime = { lastError: undefined } -    if @func -      items = {} -      items[key] = {} -      @func( items, 'synced storage stub' ) - -  mkKeyValue: (key, value) -> -    obj = {} -    obj[key] = { newValue: value } -    obj - -global.chrome.storage.sync ||= -  store: {} - -  set: (items,callback) -> -    chrome.runtime = { lastError: undefined } -    for own key, value of items -      @store[key] = value -    if callback -      callback() -    # Now, generate (supposedly asynchronous) notifications for listeners. -    for own key, value of items -      global.chrome.storage.onChanged.call(key,value) - -  get: (keys,callback) -> -    chrome.runtime = { lastError: undefined } -    if keys == null -      keys = [] -      for own key, value of @store -        keys.push key -    items = {} -    for key in keys -      items[key] = @store[key] -    # Now, generate (supposedly asynchronous) callback -    if callback -      callback items - -  remove: (key,callback) -> -    chrome.runtime = { lastError: undefined } -    if key of @store -      delete @store[key] -    if callback -      callback() -    # Now, generate (supposedly asynchronous) notification for listeners. -    global.chrome.storage.onChanged.callEmpty(key) - +global.chrome = +  runtime: {} + +  storage: + +    # chrome.storage.onChanged +    onChanged: +      addListener: (func) -> @func = func + +      # Fake a callback from chrome.storage.sync. +      call: (key, value) -> +        chrome.runtime = { lastError: undefined } +        key_value = {} +        key_value[key] = { newValue: value } +        @func(key_value,'synced storage stub') if @func + +      callEmpty: (key) -> +        chrome.runtime = { lastError: undefined } +        if @func +          items = {} +          items[key] = {} +          @func(items,'synced storage stub') + +    # chrome.storage.sync +    sync: +      store: {} + +      set: (items, callback) -> +        chrome.runtime = { lastError: undefined } +        for own key, value of items +          @store[key] = value +        callback() if callback +        # Now, generate (supposedly asynchronous) notifications for listeners. +        for own key, value of items +          global.chrome.storage.onChanged.call(key,value) + +      get: (keys, callback) -> +        chrome.runtime = { lastError: undefined } +        if keys == null +          keys = [] +          for own key, value of @store +            keys.push key +        items = {} +        for key in keys +          items[key] = @store[key] +        # Now, generate (supposedly asynchronous) callback +        callback items if callback + +      remove: (key, callback) -> +        chrome. runtime = { lastError: undefined } +        if key of @store +          delete @store[key] +        callback() if callback +        # Now, generate (supposedly asynchronous) notification for listeners. +        global.chrome.storage.onChanged.callEmpty(key)  | 
