Commit f1b019f0 authored by Randall Leeds's avatar Randall Leeds

Switch out jschannel for frame-rpc

The frame-rpc module is a proper CommonJS module and it's footprint
is really tiny. Its protocol is simpler. It doesn't handle connection
and buffering, but with our onConnect callback we don't need buffering.
The connection handshake that jschannel did was so trivial it's been
re-implemented here (each side tries to connect to the other once).

We have to handle timeouts ourselves, but that's all hidden in the
bridge code. In exchange, we get a simpler API, we get rid of the
call/notify distinction in favor of just passing a callback or not
and we avoid the excess overhead of the recursion guard in the
serialization code that was giving us false positive issues with
the document title.

This is one step toward removing all the browserify-shim requiring
libraries from the injected bundle, which will eventually fix #2397.
parent 7372f1b7
......@@ -58,49 +58,47 @@ module.exports = class AnnotationSync
getAnnotationForTag: (tag) ->
@cache[tag] or null
sync: (annotations, cb) ->
sync: (annotations) ->
annotations = (this._format a for a in annotations)
@bridge.call
method: 'sync'
params: annotations
callback: cb
@bridge.call 'sync', annotations, (err, annotations = []) =>
for a in annotations
this._parse(a)
this
# Handlers for messages arriving through a channel
_channelListeners:
'beforeCreateAnnotation': (txn, body) ->
'beforeCreateAnnotation': (body, cb) ->
annotation = this._parse(body)
delete @cache[annotation.$$tag]
@_emit 'beforeAnnotationCreated', annotation
@cache[annotation.$$tag] = annotation
this._format annotation
cb(null, this._format(annotation))
'createAnnotation': (txn, body) ->
'createAnnotation': (body, cb) ->
annotation = this._parse(body)
delete @cache[annotation.$$tag]
@_emit 'annotationCreated', annotation
@cache[annotation.$$tag] = annotation
this._format annotation
cb(null, this._format(annotation))
'updateAnnotation': (txn, body) ->
'updateAnnotation': (body, cb) ->
annotation = this._parse(body)
delete @cache[annotation.$$tag]
@_emit('beforeAnnotationUpdated', annotation)
@_emit('annotationUpdated', annotation)
@cache[annotation.$$tag] = annotation
this._format annotation
cb(null, this._format(annotation))
'deleteAnnotation': (txn, body) ->
'deleteAnnotation': (body, cb) ->
annotation = this._parse(body)
delete @cache[annotation.$$tag]
@_emit('annotationDeleted', annotation)
res = this._format(annotation)
res
cb(null, this._format(annotation))
'sync': (ctx, bodies) ->
(this._format(this._parse(b)) for b in bodies)
'sync': (bodies, cb) ->
cb(null, (this._format(this._parse(b)) for b in bodies))
'loadAnnotations': (txn, bodies) ->
'loadAnnotations': (bodies) ->
annotations = (this._parse(a) for a in bodies)
@_emit('loadAnnotations', annotations)
......@@ -127,17 +125,13 @@ module.exports = class AnnotationSync
'annotationsLoaded': (annotations) ->
bodies = (this._format a for a in annotations when not a.$$tag)
return unless bodies.length
@bridge.notify
method: 'loadAnnotations'
params: bodies
@bridge.call('loadAnnotations', bodies)
_syncCache: (channel) ->
# Synchronise (here to there) the items in our cache
annotations = (this._format a for t, a of @cache)
if annotations.length
channel.notify
method: 'loadAnnotations'
params: annotations
channel.call('loadAnnotations', annotations)
_mkCallRemotelyAndParseResults: (method, callBack) ->
(annotation) =>
......@@ -148,11 +142,7 @@ module.exports = class AnnotationSync
callBack? failure, results
# Call the remote method
options =
method: method
callback: wrappedCallback
params: this._format(annotation)
@bridge.call(options)
@bridge.call(method, this._format(annotation), wrappedCallback)
# Parse returned message bodies to update cache with any changes made remotely
_parseResults: (results) ->
......
......@@ -18,18 +18,18 @@ module.exports = class AnnotationUISync
tags.map(annotationSync.getAnnotationForTag, annotationSync)
channelListeners =
showAnnotations: (ctx, tags=[]) ->
showAnnotations: (tags=[]) ->
annotations = getAnnotationsByTags(tags)
annotationUI.selectAnnotations(annotations)
focusAnnotations: (ctx, tags=[]) ->
focusAnnotations: (tags=[]) ->
annotations = getAnnotationsByTags(tags)
annotationUI.focusAnnotations(annotations)
toggleAnnotationSelection: (ctx, tags=[]) ->
toggleAnnotationSelection: (tags=[]) ->
annotations = getAnnotationsByTags(tags)
annotationUI.xorSelectedAnnotations(annotations)
setVisibleHighlights: (ctx, state) ->
setVisibleHighlights: (state) ->
annotationUI.visibleHighlights = Boolean(state)
bridge.notify(method: 'setVisibleHighlights', params: state)
bridge.call('setVisibleHighlights', state)
# Because the channel events are all outside of the angular framework we
# need to inform Angular that it needs to re-check it's state and re-draw
......@@ -45,8 +45,6 @@ module.exports = class AnnotationUISync
onConnect = (channel, source) ->
# Allow the host to define its own state
unless source is $window.parent
channel.notify
method: 'setVisibleHighlights'
params: annotationUI.visibleHighlights
channel.call('setVisibleHighlights', annotationUI.visibleHighlights)
bridge.onConnect(onConnect)
......@@ -72,10 +72,6 @@ module.exports = class Guest extends Annotator
formatted = {}
for k, v of annotation when k isnt 'anchors'
formatted[k] = v
# Work around issue in jschannel where a repeated object is considered
# recursive, even if it is not its own ancestor.
if formatted.document?.title
formatted.document.title = formatted.document.title.slice()
formatted
this.addPlugin('CrossFrame', cfOptions)
......@@ -115,21 +111,20 @@ module.exports = class Guest extends Annotator
crossframe.onConnect(=> this.publish('panelReady'))
crossframe.on('onEditorHide', this.onEditorHide)
crossframe.on('onEditorSubmit', this.onEditorSubmit)
crossframe.on 'focusAnnotations', (ctx, tags=[]) =>
crossframe.on 'focusAnnotations', (tags=[]) =>
for anchor in @anchors when anchor.highlights?
toggle = anchor.annotation.$$tag in tags
$(anchor.highlights).toggleClass('annotator-hl-focused', toggle)
crossframe.on 'scrollToAnnotation', (ctx, tag) =>
crossframe.on 'scrollToAnnotation', (tag) =>
for anchor in @anchors when anchor.highlights?
if anchor.annotation.$$tag is tag
$(anchor.highlights).scrollintoview()
return
crossframe.on 'getDocumentInfo', (trans) =>
trans.delayReturn(true)
crossframe.on 'getDocumentInfo', (cb) =>
this.getDocumentInfo()
.then((info) -> trans.complete(info))
.catch((reason) -> trans.error(reason))
crossframe.on 'setVisibleHighlights', (ctx, state) =>
.then((info) -> cb(null, info))
.catch((reason) -> cb(reason))
crossframe.on 'setVisibleHighlights', (state) =>
this.publish 'setVisibleHighlights', state
_setupWrapper: ->
......@@ -315,24 +310,20 @@ module.exports = class Guest extends Annotator
return annotation
showAnnotations: (annotations) =>
@crossframe?.notify
method: "showAnnotations"
params: (a.$$tag for a in annotations)
tags = (a.$$tag for a in annotations)
@crossframe?.call('showAnnotations', tags)
toggleAnnotationSelection: (annotations) =>
@crossframe?.notify
method: "toggleAnnotationSelection"
params: (a.$$tag for a in annotations)
tags = (a.$$tag for a in annotations)
@crossframe?.call('toggleAnnotationSelection', tags)
updateAnnotations: (annotations) =>
@crossframe?.notify
method: "updateAnnotations"
params: (a.$$tag for a in annotations)
tags = (a.$$tag for a in annotations)
@crossframe?.call('updateAnnotations', tags)
focusAnnotations: (annotations) =>
@crossframe?.notify
method: "focusAnnotations"
params: (a.$$tag for a in annotations)
tags = (a.$$tag for a in annotations)
@crossframe?.call('focusAnnotations', tags)
onSuccessfulSelection: (event, immediate) ->
unless event?
......@@ -396,11 +387,7 @@ module.exports = class Guest extends Annotator
# Pass true to show the highlights in the frame or false to disable.
setVisibleHighlights: (shouldShowHighlights) ->
return if @visibleHighlights == shouldShowHighlights
@crossframe?.notify
method: 'setVisibleHighlights'
params: shouldShowHighlights
@crossframe?.call('setVisibleHighlights', shouldShowHighlights)
this.toggleHighlightClass(shouldShowHighlights)
toggleHighlightClass: (shouldShowHighlights) ->
......@@ -413,11 +400,11 @@ module.exports = class Guest extends Annotator
# Open the sidebar
showFrame: ->
@crossframe?.notify method: 'open'
@crossframe?.call('open')
# Close the sidebar
hideFrame: ->
@crossframe?.notify method: 'back'
@crossframe?.call('back')
onAdderMouseup: (event) ->
event.preventDefault()
......
......@@ -12,7 +12,7 @@ extract = extract = (obj, keys...) ->
# as keeping the annotation state in sync with the sidebar application, this
# frame acts as the bridge client, the sidebar is the server. This plugin
# can also be used to send messages through to the sidebar using the
# notify method.
# call method.
module.exports = class CrossFrame extends Annotator.Plugin
constructor: (elem, options) ->
super
......@@ -41,8 +41,8 @@ module.exports = class CrossFrame extends Annotator.Plugin
this.on = (event, fn) ->
bridge.on(event, fn)
this.notify = (message) ->
bridge.notify(message)
this.call = (message, args...) ->
bridge.call(message, args...)
this.onConnect = (fn) ->
bridge.onConnect(fn)
......@@ -21,7 +21,7 @@ describe 'Annotator.Plugin.CrossFrame', ->
fakeBridge =
createChannel: sandbox.stub()
onConnect: sandbox.stub()
notify: sandbox.stub()
call: sandbox.stub()
on: sandbox.stub()
fakeAnnotationSync =
......@@ -91,11 +91,11 @@ describe 'Annotator.Plugin.CrossFrame', ->
bridge.on('event', 'arg')
assert.calledWith(fakeBridge.on, 'event', 'arg')
describe '.notify', ->
describe '.call', ->
it 'proxies the call to the bridge', ->
bridge = createCrossFrame()
bridge.notify(method: 'method')
assert.calledWith(fakeBridge.notify, method: 'method')
bridge.call('method', 'arg1', 'arg2')
assert.calledWith(fakeBridge.call, 'method', 'arg1', 'arg2')
describe '.onConnect', ->
it 'proxies the call to the bridge', ->
......
......@@ -144,18 +144,11 @@ describe 'Guest', ->
formatted = options.formatter(ann)
assert.equal(formatted.$$tag, 'tag1')
it 'strips the "anchors" property', ->
it 'strips properties that are not whitelisted', ->
ann = {$$tag: 'tag1', anchors: []}
formatted = options.formatter(ann)
assert.notProperty(formatted, 'anchors')
it 'clones the document.title array if present', ->
title = ['Page Title']
ann = {$$tag: 'tag1', document: {title: title}}
formatted = options.formatter(ann)
assert.notStrictEqual(title, formatted.document.title)
assert.deepEqual(title, formatted.document.title)
describe 'annotation UI events', ->
emitGuestEvent = (event, args...) ->
fn(args...) for [evt, fn] in fakeCrossFrame.on.args when event == evt
......@@ -183,7 +176,7 @@ describe 'Guest', ->
{annotation: {$$tag: 'tag1'}, highlights: highlight0.toArray()}
{annotation: {$$tag: 'tag2'}, highlights: highlight1.toArray()}
]
emitGuestEvent('focusAnnotations', 'ctx', ['tag1'])
emitGuestEvent('focusAnnotations', ['tag1'])
assert.isTrue(highlight0.hasClass('annotator-hl-focused'))
it 'unfocuses any annotations without a matching tag', ->
......@@ -210,7 +203,7 @@ describe 'Guest', ->
guest.anchors = [
{annotation: {$$tag: 'tag1'}, highlights: highlight.toArray()}
]
emitGuestEvent('scrollToAnnotation', 'ctx', 'tag1')
emitGuestEvent('scrollToAnnotation', 'tag1')
assert.calledOn($.fn.scrollintoview, sinon.match(highlight))
describe 'on "getDocumentInfo" event', ->
......@@ -227,7 +220,7 @@ describe 'Guest', ->
sandbox.restore()
it 'calls the callback with the href and pdf metadata', (done) ->
assertComplete = (payload) ->
assertComplete = (err, payload) ->
try
assert.equal(payload.uri, document.location.href)
assert.equal(payload.metadata, metadata)
......@@ -235,15 +228,14 @@ describe 'Guest', ->
catch e
done(e)
ctx = {complete: assertComplete, delayReturn: sandbox.stub()}
metadata = {title: 'hi'}
promise = Promise.resolve(metadata)
guest.plugins.PDF.getMetadata.returns(promise)
emitGuestEvent('getDocumentInfo', ctx)
emitGuestEvent('getDocumentInfo', assertComplete)
it 'calls the callback with the href and basic metadata if pdf fails', (done) ->
assertComplete = (payload) ->
assertComplete = (err, payload) ->
try
assert.equal(payload.uri, window.location.href)
assert.deepEqual(payload.metadata, metadata)
......@@ -251,20 +243,11 @@ describe 'Guest', ->
catch e
done(e)
ctx = {complete: assertComplete, delayReturn: sandbox.stub()}
metadata = {title: 'hi', link: [{href: window.location.href}]}
promise = Promise.reject(new Error('Not a PDF document'))
guest.plugins.PDF.getMetadata.returns(promise)
emitGuestEvent('getDocumentInfo', ctx)
it 'notifies the channel that the return value is async', ->
delete guest.plugins.PDF
ctx = {complete: sandbox.stub(), delayReturn: sandbox.stub()}
emitGuestEvent('getDocumentInfo', ctx)
assert.calledWith(ctx.delayReturn, true)
emitGuestEvent('getDocumentInfo', assertComplete)
describe 'onAdderMouseUp', ->
it 'it prevents the default browser action when triggered', () ->
......
......@@ -16,7 +16,7 @@ describe 'Host', ->
fakeCrossFrame = {}
fakeCrossFrame.onConnect = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.on = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.notify = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.call = sandbox.spy()
Annotator.Plugin.CrossFrame = -> fakeCrossFrame
......
$ = require('jquery')
Channel = require('jschannel')
extend = require('extend')
RPC = require('frame-rpc')
# The Bridge service sets up a channel between frames
# and provides an events API on top of it.
......@@ -14,20 +14,28 @@ module.exports = class Bridge
@channelListeners = {}
@onConnectListeners = []
createChannel: (source, origin, scope) ->
createChannel: (source, origin, token) ->
channel = null
connected = false
ready = =>
return if connected
connected = true
for cb in @onConnectListeners
cb.call(null, channel, source)
connect = (_token, cb) =>
if _token is token
cb()
ready()
listeners = extend({connect}, @channelListeners)
# Set up a channel
channelOptions =
window: source
origin: origin
scope: scope
onReady: (channel) =>
for callback in @onConnectListeners
callback.call(this, channel, source)
channel = this._buildChannel channelOptions
channel = new RPC(window, source, origin, listeners)
# Attach channel message listeners
for own method, callback of @channelListeners
channel.bind method, callback
# Fire off a connection attempt
channel.call('connect', token, ready)
# Store the newly created channel in our collection
@links.push
......@@ -38,55 +46,50 @@ module.exports = class Bridge
# Make a method call on all links, collect the results and pass them to a
# callback when all results are collected. Parameters:
# - options.method (required): name of remote method to call
# - options.params: parameters to pass to remote method
# - options.callback: called with array of results
call: (options) ->
# - method (required): name of remote method to call
# - args...: parameters to pass to remote method
# - callback: (optional) called with error, if any, and an Array of results
call: (method, args...) ->
cb = null
if typeof(args[args.length - 1]) is 'function'
cb = args[args.length - 1]
args = args.slice(0, -1)
_makeDestroyFn = (c) =>
(error, reason) =>
(error) =>
c.destroy()
@links = (l for l in @links when l.channel isnt c)
throw error
deferreds = @links.map (l) ->
d = $.Deferred().fail(_makeDestroyFn l.channel)
callOptions = {
method: options.method
params: options.params
success: (result) -> d.resolve result
error: (error, reason) ->
if error isnt 'timeout_error'
d.reject error, reason
else
d.resolve null
timeout: 1000
}
l.channel.call callOptions
d.promise()
$.when(deferreds...)
.then (results...) =>
options.callback? null, results
.fail (failure) =>
options.callback? failure
# Publish a notification to all links
notify: (options) ->
for l in @links
l.channel.notify options
return
promises = @links.map (l) ->
p = new Promise (resolve, reject) ->
timeout = setTimeout((-> resolve(null)), 1000)
try
l.channel.call method, args..., (err, result) ->
clearTimeout(timeout)
if err then reject(err) else resolve(result)
catch err
reject(err)
# Don't assign here. The disconnect is handled asynchronously.
return p.catch(_makeDestroyFn(l.channel))
resultPromise = Promise.all(promises)
if cb?
resultPromise = resultPromise
.then((results) -> cb(null, results))
.catch((error) -> cb(error))
return resultPromise
on: (method, callback) ->
if @channelListeners[method]
throw new Error("Listener '#{method}' already bound in Bridge")
@channelListeners[method] = callback
for l in @links
l.channel.bind method, callback
return this
off: (method) ->
for l in @links
l.channel.unbind method
delete @channelListeners[method]
return this
......@@ -94,11 +97,3 @@ module.exports = class Bridge
onConnect: (callback) ->
@onConnectListeners.push(callback)
this
# Construct a channel to another frame
_buildChannel: (options) ->
# jschannel chokes on FF and Chrome extension origins.
if (options.origin.match /^chrome-extension:\/\//) or
(options.origin.match /^resource:\/\//)
options = $.extend {}, options, {origin: '*'}
channel = Channel.build(options)
......@@ -45,9 +45,10 @@ module.exports = class CrossFrame
new AnnotationUISync($rootScope, $window, bridge, annotationSync, annotationUI)
addFrame = (channel) =>
channel.call
method: 'getDocumentInfo'
success: (info) =>
channel.call 'getDocumentInfo', (err, info) =>
if err
channel.destroy()
else
$rootScope.$apply =>
@frames.push({channel: channel, uri: info.uri})
......@@ -62,6 +63,6 @@ module.exports = class CrossFrame
bridge.createChannel(source, origin, token)
discovery.startDiscovery(onDiscoveryCallback)
this.notify = bridge.notify.bind(bridge)
this.call = bridge.call.bind(bridge)
this.notify = -> throw new Error('connect() must be called before notify()')
this.call = -> throw new Error('connect() must be called before call()')
......@@ -11,13 +11,13 @@ module.exports = [
'$window', 'bridge'
($window, bridge) ->
host =
showSidebar: -> notifyHost method: 'showFrame'
hideSidebar: -> notifyHost method: 'hideFrame'
showSidebar: -> callHost('showFrame')
hideSidebar: -> callHost('hideFrame')
# Sends a message to the host frame
notifyHost = (message) ->
callHost = (method) ->
for {channel, window} in bridge.links when window is $window.parent
channel.notify(message)
channel.call(method)
break
channelListeners =
......
......@@ -5,7 +5,7 @@ describe 'AnnotationSync', ->
publish = null
fakeBridge = null
createAnnotationSync = null
createChannel = -> {notify: sandbox.stub()}
createChannel = -> {call: sandbox.stub()}
options = null
PARENT_WINDOW = 'PARENT_WINDOW'
......@@ -16,13 +16,12 @@ describe 'AnnotationSync', ->
beforeEach module('h')
beforeEach inject (AnnotationSync, $rootScope) ->
listeners = {}
publish = ({method, params}) -> listeners[method]('ctx', params)
publish = (method, args...) -> listeners[method](args...)
fakeWindow = parent: PARENT_WINDOW
fakeBridge =
on: sandbox.spy((method, fn) -> listeners[method] = fn)
call: sandbox.stub()
notify: sandbox.stub()
onConnect: sandbox.stub()
links: []
......@@ -47,11 +46,9 @@ describe 'AnnotationSync', ->
channel = createChannel()
fakeBridge.onConnect.yield(channel)
assert.called(channel.notify)
assert.calledWith(channel.notify, {
method: 'loadAnnotations'
params: [tag: 'tag1', msg: ann]
})
assert.called(channel.call)
assert.calledWith(channel.call, 'loadAnnotations',
[tag: 'tag1', msg: ann])
it 'does nothing if the cache is empty', ->
annSync = createAnnotationSync()
......@@ -59,7 +56,7 @@ describe 'AnnotationSync', ->
channel = createChannel()
fakeBridge.onConnect.yield(channel)
assert.notCalled(channel.notify)
assert.notCalled(channel.call)
describe '.getAnnotationForTag', ->
it 'returns the annotation if present in the cache', ->
......@@ -80,18 +77,20 @@ describe 'AnnotationSync', ->
it 'broadcasts the "' + publishEvent + '" event over the local event bus', ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
publish(method: channelEvent, params: {msg: ann})
publish(channelEvent, {msg: ann}, ->)
assert.called(options.emit)
assert.calledWith(options.emit, publishEvent, ann)
assertReturnValue = (channelEvent) ->
it 'returns a formatted annotation to be sent to the calling frame', ->
it 'calls back with a formatted annotation', (done) ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
ret = publish(method: channelEvent, params: {msg: ann})
assert.deepEqual(ret, {tag: 'tag1', msg: ann})
callback = (err, ret) ->
assert.isNull(err)
assert.deepEqual(ret, {tag: 'tag1', msg: ann})
done()
publish(channelEvent, {msg: ann}, callback)
assertCacheState = (channelEvent) ->
it 'removes an existing entry from the cache before the event is triggered', ->
......@@ -101,13 +100,13 @@ describe 'AnnotationSync', ->
annSync = createAnnotationSync()
annSync.cache['tag1'] = ann
publish(method: channelEvent, params: {msg: ann})
publish(channelEvent, {msg: ann}, ->)
it 'ensures the annotation is inserted in the cache', ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
publish(method: channelEvent, params: {msg: ann})
publish(channelEvent, {msg: ann}, ->)
assert.equal(annSync.cache['tag1'], ann)
......@@ -138,29 +137,33 @@ describe 'AnnotationSync', ->
annSync = createAnnotationSync()
annSync.cache['tag1'] = ann
publish(method: 'deleteAnnotation', params: {msg: ann})
publish('deleteAnnotation', {msg: ann}, ->)
it 'removes the annotation from the cache', ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
publish(method: 'deleteAnnotation', params: {msg: ann})
publish('deleteAnnotation', {msg: ann}, ->)
assert(!annSync.cache['tag1'])
describe 'the "sync" event', ->
it 'returns an array of parsed and formatted annotations', ->
it 'calls back with parsed and formatted annotations', (done) ->
options.parser = sinon.spy((x) -> x)
options.formatter = sinon.spy((x) -> x)
annSync = createAnnotationSync()
annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3, $$tag: 'tag3'}]
bodies = ({msg: ann, tag: ann.$$tag} for ann in annotations)
ret = publish(method: 'sync', params: bodies)
assert.deepEqual(ret, ret)
assert.called(options.parser)
assert.called(options.formatter)
callback = (err, ret) ->
assert.isNull(err)
assert.deepEqual(ret, bodies)
assert.called(options.parser)
assert.called(options.formatter)
done()
publish('sync', bodies, callback)
describe 'the "loadAnnotations" event', ->
it 'publishes the "loadAnnotations" event with parsed annotations', ->
......@@ -169,7 +172,7 @@ describe 'AnnotationSync', ->
annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3, $$tag: 'tag3'}]
bodies = ({msg: ann, tag: ann.$$tag} for ann in annotations)
ret = publish(method: 'loadAnnotations', params: bodies)
publish('loadAnnotations', bodies, ->)
assert.called(options.parser)
assert.calledWith(options.emit, 'loadAnnotations', annotations)
......@@ -182,11 +185,8 @@ describe 'AnnotationSync', ->
options.emit('beforeAnnotationCreated', ann)
assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, {
method: 'beforeCreateAnnotation',
params: {msg: ann, tag: ann.$$tag},
callback: sinon.match.func
})
assert.calledWith(fakeBridge.call, 'beforeCreateAnnotation',
{msg: ann, tag: ann.$$tag}, sinon.match.func)
it 'returns early if the annotation has a tag', ->
ann = {id: 1, $$tag: 'tag1'}
......@@ -203,11 +203,8 @@ describe 'AnnotationSync', ->
options.emit('annotationCreated', ann)
assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, {
method: 'createAnnotation',
params: {msg: ann, tag: ann.$$tag},
callback: sinon.match.func
})
assert.calledWith(fakeBridge.call, 'createAnnotation',
{msg: ann, tag: ann.$$tag}, sinon.match.func)
it 'returns early if the annotation has a tag but is not cached', ->
ann = {id: 1, $$tag: 'tag1'}
......@@ -231,11 +228,8 @@ describe 'AnnotationSync', ->
options.emit('annotationUpdated', ann)
assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, {
method: 'updateAnnotation',
params: {msg: ann, tag: ann.$$tag},
callback: sinon.match.func
})
assert.calledWith(fakeBridge.call, 'updateAnnotation',
{msg: ann, tag: ann.$$tag}, sinon.match.func)
it 'returns early if the annotation has a tag but is not cached', ->
ann = {id: 1, $$tag: 'tag1'}
......@@ -259,11 +253,8 @@ describe 'AnnotationSync', ->
options.emit('annotationDeleted', ann)
assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, {
method: 'deleteAnnotation',
params: {msg: ann, tag: ann.$$tag},
callback: sinon.match.func
})
assert.calledWith(fakeBridge.call, 'deleteAnnotation',
{msg: ann, tag: ann.$$tag}, sinon.match.func)
it 'parses the result returned by the call', ->
ann = {id: 1, $$tag: 'tag1'}
......@@ -273,7 +264,7 @@ describe 'AnnotationSync', ->
options.emit('annotationDeleted', ann)
body = {msg: {}, tag: 'tag1'}
fakeBridge.call.yieldTo('callback', null, [body])
fakeBridge.call.yield(null, [body])
assert.called(options.parser)
assert.calledWith(options.parser, {})
......@@ -283,7 +274,7 @@ describe 'AnnotationSync', ->
annSync.cache.tag1 = ann
options.emit('annotationDeleted', ann)
fakeBridge.call.yieldTo('callback', null, [])
fakeBridge.call.yield(null, [])
assert.isUndefined(annSync.cache.tag1)
it 'does not remove the annotation from the cache if an error occurs', ->
......@@ -292,7 +283,7 @@ describe 'AnnotationSync', ->
annSync.cache.tag1 = ann
options.emit('annotationDeleted', ann)
fakeBridge.call.yieldTo('callback', new Error('Error'), [])
fakeBridge.call.yield(new Error('Error'), [])
assert.equal(annSync.cache.tag1, ann)
it 'returns early if the annotation has a tag but is not cached', ->
......@@ -326,11 +317,9 @@ describe 'AnnotationSync', ->
annSync = createAnnotationSync()
options.emit('annotationsLoaded', annotations)
assert.called(fakeBridge.notify)
assert.calledWith(fakeBridge.notify, {
method: 'loadAnnotations',
params: {msg: a, tag: a.$$tag} for a in annotations
})
assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, 'loadAnnotations',
({msg: a, tag: a.$$tag} for a in annotations))
it 'does not send annotations that have already been tagged', ->
annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3}]
......@@ -338,14 +327,12 @@ describe 'AnnotationSync', ->
annSync = createAnnotationSync()
options.emit('annotationsLoaded', annotations)
assert.called(fakeBridge.notify)
assert.calledWith(fakeBridge.notify, {
method: 'loadAnnotations',
params: [{msg: annotations[2], tag: annotations[2].$$tag}]
})
assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, 'loadAnnotations',
[{msg: annotations[2], tag: annotations[2].$$tag}])
it 'returns early if no annotations are loaded', ->
annSync = createAnnotationSync()
options.emit('annotationsLoaded', [])
assert.notCalled(fakeBridge.notify)
assert.notCalled(fakeBridge.call)
......@@ -9,7 +9,7 @@ describe 'AnnotationUISync', ->
fakeAnnotationUI = null
fakeAnnotationSync = null
createAnnotationUISync = null
createChannel = -> {notify: sandbox.stub()}
createChannel = -> {call: sandbox.stub()}
PARENT_WINDOW = 'PARENT_WINDOW'
before ->
......@@ -20,12 +20,12 @@ describe 'AnnotationUISync', ->
beforeEach inject (AnnotationUISync, $rootScope) ->
$digest = sandbox.stub($rootScope, '$digest')
listeners = {}
publish = ({method, params}) -> listeners[method]('ctx', params)
publish = (method, args...) -> listeners[method](args...)
fakeWindow = parent: PARENT_WINDOW
fakeBridge =
on: sandbox.spy((method, fn) -> listeners[method] = fn)
notify: sandbox.stub()
call: sandbox.stub()
onConnect: sandbox.stub()
links: [
{window: PARENT_WINDOW, channel: createChannel()}
......@@ -54,26 +54,20 @@ describe 'AnnotationUISync', ->
createAnnotationUISync()
assert.calledWith(channel.notify, {
method: 'setVisibleHighlights'
params: false
})
assert.calledWith(channel.call, 'setVisibleHighlights', false)
describe 'when the source is the parent window', ->
it 'does nothing', ->
channel = notify: sandbox.stub()
channel = call: sandbox.stub()
fakeBridge.onConnect.callsArgWith(0, channel, PARENT_WINDOW)
createAnnotationUISync()
assert.notCalled(channel.notify)
assert.notCalled(channel.call)
describe 'on "showAnnotations" event', ->
it 'updates the annotationUI to include the shown annotations', ->
createAnnotationUISync()
publish({
method: 'showAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
publish('showAnnotations', ['tag1', 'tag2', 'tag3'])
assert.called(fakeAnnotationUI.selectAnnotations)
assert.calledWith(fakeAnnotationUI.selectAnnotations, [
{id: 1}, {id: 2}, {id: 3}
......@@ -81,19 +75,13 @@ describe 'AnnotationUISync', ->
it 'triggers a digest', ->
createAnnotationUISync()
publish({
method: 'showAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
publish('showAnnotations', ['tag1', 'tag2', 'tag3'])
assert.called($digest)
describe 'on "focusAnnotations" event', ->
it 'updates the annotationUI to show the provided annotations', ->
createAnnotationUISync()
publish({
method: 'focusAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
publish('focusAnnotations', ['tag1', 'tag2', 'tag3'])
assert.called(fakeAnnotationUI.focusAnnotations)
assert.calledWith(fakeAnnotationUI.focusAnnotations, [
{id: 1}, {id: 2}, {id: 3}
......@@ -101,19 +89,13 @@ describe 'AnnotationUISync', ->
it 'triggers a digest', ->
createAnnotationUISync()
publish({
method: 'focusAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
publish('focusAnnotations', ['tag1', 'tag2', 'tag3'])
assert.called($digest)
describe 'on "toggleAnnotationSelection" event', ->
it 'updates the annotationUI to show the provided annotations', ->
createAnnotationUISync()
publish({
method: 'toggleAnnotationSelection',
params: ['tag1', 'tag2', 'tag3']
})
publish('toggleAnnotationSelection', ['tag1', 'tag2', 'tag3'])
assert.called(fakeAnnotationUI.xorSelectedAnnotations)
assert.calledWith(fakeAnnotationUI.xorSelectedAnnotations, [
{id: 1}, {id: 2}, {id: 3}
......@@ -121,36 +103,21 @@ describe 'AnnotationUISync', ->
it 'triggers a digest', ->
createAnnotationUISync()
publish({
method: 'toggleAnnotationSelection',
params: ['tag1', 'tag2', 'tag3']
})
publish('toggleAnnotationSelection', ['tag1', 'tag2', 'tag3'])
assert.called($digest)
describe 'on "setVisibleHighlights" event', ->
it 'updates the annotationUI with the new value', ->
createAnnotationUISync()
publish({
method: 'setVisibleHighlights',
params: true
})
publish('setVisibleHighlights', true)
assert.equal(fakeAnnotationUI.visibleHighlights, true)
it 'notifies the other frames of the change', ->
createAnnotationUISync()
publish({
method: 'setVisibleHighlights',
params: true
})
assert.calledWith(fakeBridge.notify, {
method: 'setVisibleHighlights'
params: true
})
publish('setVisibleHighlights', true)
assert.calledWith(fakeBridge.call, 'setVisibleHighlights', true)
it 'triggers a digest of the application state', ->
createAnnotationUISync()
publish({
method: 'setVisibleHighlights',
params: true
})
publish('setVisibleHighlights', true)
assert.called($digest)
This diff is collapsed.
......@@ -26,7 +26,7 @@ describe 'CrossFrame', ->
fakeDiscovery =
startDiscovery: sandbox.stub()
fakeBridge =
notify: sandbox.stub()
call: sandbox.stub()
createChannel: sandbox.stub()
onConnect: sandbox.stub()
fakeAnnotationSync = {}
......@@ -61,28 +61,16 @@ describe 'CrossFrame', ->
it 'queries discovered frames for metadata', ->
uri = 'http://example.com'
channel = {call: sandbox.stub().yieldsTo('success', {uri: uri})}
channel = {call: sandbox.stub().yields(null, {uri: uri})}
fakeBridge.onConnect.yields(channel)
crossframe.connect()
assert.calledWith(channel.call, {
method: 'getDocumentInfo'
success: sinon.match.func
})
assert.calledWith(channel.call, 'getDocumentInfo', sinon.match.func)
it 'updates the frames array', ->
uri = 'http://example.com'
channel = {call: sandbox.stub().yieldsTo('success', {uri: uri})}
channel = {call: sandbox.stub().yields(null, {uri: uri})}
fakeBridge.onConnect.yields(channel)
crossframe.connect()
assert.deepEqual(crossframe.frames, [
{channel: channel, uri: uri}
])
describe '.notify()', ->
it 'proxies the call to the bridge', ->
message = {method: 'foo', params: 'bar'}
crossframe.connect() # create the bridge.
crossframe.notify(message)
assert.calledOn(fakeBridge.notify, fakeBridge)
assert.calledWith(fakeBridge.notify, message)
......@@ -3,7 +3,7 @@
describe 'host', ->
sandbox = null
host = null
createChannel = -> notify: sandbox.stub()
createChannel = -> call: sandbox.stub()
fakeBridge = null
$digest = null
publish = null
......@@ -22,13 +22,13 @@ describe 'host', ->
listeners = {}
publish = ({method, params}) ->
listeners[method]('ctx', params)
publish = (method, args...) ->
listeners[method](args...)
fakeBridge =
ls: listeners
on: sandbox.spy (method, fn) -> listeners[method] = fn
notify: sandbox.stub()
call: sandbox.stub()
onConnect: sandbox.stub()
links: [
{window: PARENT_WINDOW, channel: createChannel()}
......@@ -53,16 +53,16 @@ describe 'host', ->
describe 'showSidebar()', ->
it 'sends the "showFrame" message to the host only', ->
host.showSidebar()
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'showFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
assert.calledWith(fakeBridge.links[0].channel.call, 'showFrame')
assert.notCalled(fakeBridge.links[1].channel.call)
assert.notCalled(fakeBridge.links[2].channel.call)
describe 'hideSidebar()', ->
it 'sends the "hideFrame" message to the host only', ->
host.hideSidebar()
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'hideFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
assert.calledWith(fakeBridge.links[0].channel.call, 'hideFrame')
assert.notCalled(fakeBridge.links[1].channel.call)
assert.notCalled(fakeBridge.links[2].channel.call)
describe 'reacting to the bridge', ->
......@@ -70,12 +70,12 @@ describe 'host', ->
it 'triggers the hideSidebar() API', ->
sandbox.spy host, "hideSidebar"
publish method: 'back'
publish 'back'
assert.called host.hideSidebar
describe 'on "open" event', ->
it 'triggers the showSidebar() API', ->
sandbox.spy host, "showSidebar"
publish method: 'open'
publish 'open'
assert.called host.showSidebar
This diff is collapsed.
......@@ -51,15 +51,11 @@ module.exports = class WidgetController
highlights = [annotation.$$tag]
else
highlights = []
crossframe.notify
method: 'focusAnnotations'
params: highlights
crossframe.call('focusAnnotations', highlights)
$scope.scrollTo = (annotation) ->
if angular.isObject annotation
crossframe.notify
method: 'scrollToAnnotation'
params: annotation.$$tag
crossframe.call('scrollToAnnotation', annotation.$$tag)
$scope.shouldShowThread = (container) ->
if annotationUI.hasSelectedAnnotations() and not container.parent.parent
......
......@@ -25,6 +25,7 @@
"dom-seek": "^1.0.1",
"es6-promise": "^2.1.0",
"extend": "^2.0.0",
"frame-rpc": "^1.3.1",
"hammerjs": "^2.0.4",
"jquery": "1.11.1",
"jstimezonedetect": "1.0.5",
......@@ -82,8 +83,7 @@
"es6-promise": "./node_modules/es6-promise/dist/es6-promise.js",
"hammerjs": "./node_modules/hammerjs/hammer.js",
"jquery": "./node_modules/jquery/dist/jquery.js",
"jquery-scrollintoview": "./h/static/scripts/vendor/jquery.scrollintoview.js",
"jschannel": "./h/static/scripts/vendor/jschannel.js"
"jquery-scrollintoview": "./h/static/scripts/vendor/jquery.scrollintoview.js"
},
"browserify-shim": {
"annotator": {
......@@ -111,7 +111,6 @@
"depends": [
"jquery"
]
},
"jschannel": "Channel"
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment