View
@@ -1,14 +1,14 @@
"""Add the staff columns to the feature and user tables.
Revision ID: ef3059e0396
Revises: 17a69c28b6c2
Revises: 3bf1c2289e8d
Create Date: 2015-07-30 16:25:14.837823
"""
# revision identifiers, used by Alembic.
revision = 'ef3059e0396'
down_revision = '17a69c28b6c2'
down_revision = '3bf1c2289e8d'
from alembic import op
import sqlalchemy as sa
View
@@ -54,6 +54,7 @@ class Stream(Resource):
class Root(Resource):
__acl__ = [
(security.Allow, security.Authenticated, 'authenticated'),
(security.Allow, 'group:admin', 'admin'),
]
View
@@ -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) ->
View
@@ -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)
View
@@ -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()
View
@@ -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)
View
@@ -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', ->
View
@@ -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,44 +220,34 @@ 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)
done()
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)
done()
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', () ->
View
@@ -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
View
@@ -1,3 +1,5 @@
require('autofill-event')
baseURI = require('base-url')()
angular = require('angular')
require('angular-jwt')
@@ -9,9 +11,6 @@ resolve =
configureDocument = ['$provide', ($provide) ->
$provide.decorator '$document', ($delegate) ->
baseURI = $delegate.prop('baseURI')
baseURI ?= $delegate.find('base').prop('href') # fallback
baseURI ?= $delegate.prop('URL') # fallback
$delegate.prop('baseURI', baseURI)
]
@@ -96,7 +95,6 @@ module.exports = angular.module('h', [
.directive('deepCount', require('./directive/deep-count'))
.directive('formInput', require('./directive/form-input'))
.directive('formValidate', require('./directive/form-validate'))
.directive('groupList', require('./directive/group-list'))
.directive('markdown', require('./directive/markdown'))
.directive('privacy', require('./directive/privacy'))
.directive('simpleSearch', require('./directive/simple-search'))
@@ -144,6 +142,7 @@ module.exports = angular.module('h', [
.service('viewFilter', require('./view-filter'))
.factory('serviceUrl', require('./service-url'))
.factory('group', require('./group-service'))
.value('AnnotationSync', require('./annotation-sync'))
.value('AnnotationUISync', require('./annotation-ui-sync'))
@@ -158,3 +157,5 @@ module.exports = angular.module('h', [
.run(setupCrossFrame)
.run(setupStreamer)
.run(setupHost)
require('./group-list-controller')
View
@@ -1,5 +1,5 @@
$ = 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,67 +46,54 @@ 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
# Add a function to be called upon a new connection
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)
View
@@ -1,7 +1,5 @@
var querystring = require('querystring');
var JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
var authPromise = null;
var tokenPromise = null;
@@ -16,13 +14,12 @@ function fetchToken($http, $q, jwtHelper, serviceUrl, session) {
if (tokenPromise === null) {
// Set up the token request data.
var data = {
assertion: session.state.id_token,
grant_type: JWT_BEARER
assertion: session.state.csrf
};
// Skip JWT authorization for the token request itself.
var config = {
data: data,
params: data,
skipAuthorization: true,
transformRequest: function (data) {
return querystring.stringify(data);
@@ -116,19 +113,19 @@ function forgetAuthentication($q, flash, session) {
function requestAuthentication($injector, $q, $rootScope) {
return authPromise.catch(function () {
var deferred = $q.defer();
authPromise = deferred.promise;
$rootScope.$on('auth', function(event, err, data) {
if (err) {
deferred.reject(err);
} else {
$injector.invoke(fetchToken).then(function (token) {
authPromise = deferred.promise;
deferred.resolve(token);
});
}
});
return authPromise;
return deferred.promise;
});
}
View
@@ -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()')
View
@@ -43,10 +43,10 @@ errorMessage = (reason) ->
AnnotationController = [
'$scope', '$timeout', '$q', '$rootScope', '$document',
'drafts', 'flash', 'permissions', 'tags', 'time',
'annotationUI', 'annotationMapper', 'session'
'annotationUI', 'annotationMapper', 'session', 'group'
($scope, $timeout, $q, $rootScope, $document,
drafts, flash, permissions, tags, time,
annotationUI, annotationMapper, session) ->
annotationUI, annotationMapper, session, group) ->
@annotation = {}
@action = 'view'
@@ -202,6 +202,15 @@ AnnotationController = [
switch @action
when 'create'
updateDomainModel(model, @annotation)
# FIXME: Need to use a unique id here, group names are not unique.
# FIXME: Need to prevent custom group names from clashing with the
# special "public" name.
if group.focusedGroup().name in ["All", "Public", "Only Me"]
model.group = "public"
else
model.group = group.focusedGroup().name
onFulfilled = =>
$rootScope.$emit('annotationCreated', model)
@view()
View
@@ -1,3 +1,8 @@
var jstz = require('jstimezonedetect').jstz;
var moment = require('moment');
require('moment-timezone');
module.exports = ['$window', function ($window) {
return function (value, format) {
// Determine the timezone name and browser language.
@@ -6,7 +11,7 @@ module.exports = ['$window', function ($window) {
// Now make a localized date and set the language.
var momentDate = moment(value);
momentDate.lang(userLang);
momentDate.locale(userLang);
// Try to localize to the browser's timezone.
try {
View
@@ -0,0 +1,20 @@
'use strict';
angular.module('h').controller(
'GroupListCtrl', ['session', 'group', function(session, group) {
var self = this;
self.groups = function() {
return group.groups();
};
self.focusedGroup = function() {
return group.focusedGroup();
};
self.focusGroup = function(name) {
return group.focusGroup(name);
};
}
]
);
View
@@ -0,0 +1,54 @@
/**
* @ngdoc service
* @name group
*
* @description
* Get and set the UI's currently focused group.
*/
'use strict';
// @ngInject
function group(session) {
// The currently focused group. This is the group that's shown as selected
// in the groups dropdown, the annotations displayed are filtered to only
// ones that belong to this group, and any new annotations that the user
// creates will be created in this group.
var focusedGroup;
// Some "groups" are always available, regardless of what real groups (if
// any) the user is a member of.
// FIXME: If no one is logged-in then Public and Only Me shouldn't be here.
// FIXME: This service can't just return {'name': 'All'} for when the
// focused group is All, it needs to be some id that can't clash with any
// custom group name.
var globalGroups = [{'name': 'All'}, {'name': 'Public'},
{'name': 'Only Me'}];
// Return the list of available groups.
var groups = function() {
return globalGroups.concat(session.state.groups || []);
};
return {
groups: groups,
// Return the currently focused group.
focusedGroup: function() {
return focusedGroup || groups()[0];
},
// Set the named group as the currently focused group.
// FIXME: Do this by id or something else instead - two groups may have
// the same name.
focusGroup: function(name) {
for (var i = 0; i < groups().length; i++) {
var group = groups()[i];
if (group.name === name) {
focusedGroup = group;
}
}
}
};
}
module.exports = group;
View
@@ -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 =
View
@@ -24,22 +24,19 @@ module.exports = function(config) {
'../../../node_modules/angular-animate/angular-animate.js',
'../../../node_modules/angular-resource/angular-resource.js',
'../../../node_modules/angular-route/angular-route.js',
'../../../node_modules/angular-sanitize/angular-sanitize.js',
'../../../node_modules/ng-tags-input/build/ng-tags-input.min.js',
'../../../node_modules/es6-promise/dist/es6-promise.js',
'../../../node_modules/moment/min/moment-with-langs.js',
'../../../node_modules/jstimezonedetect/jstz.js',
'../../../node_modules/moment-timezone/moment-timezone.js',
'vendor/angular-bootstrap.js',
'vendor/angular-sanitize.js',
'vendor/annotator.js',
'vendor/katex.js',
'vendor/moment-timezone-data.js',
'vendor/polyfills/autofill-event.js',
'vendor/polyfills/bind.js',
'vendor/polyfills/url.js',
// These are needed until PhantomJS 2.0
'../../../node_modules/es6-promise/dist/es6-promise.js',
'vendor/bind.js',
'vendor/url.js',
// Test deps
'vendor/angular-mocks.js',
'../../../node_modules/angular-mocks/angular-mocks.js',
'../../templates/client/*.html',
'test/bootstrap.js',
View
@@ -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,19 +46,17 @@ 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()
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,26 +317,22 @@ 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}]
options.formatter = sinon.spy((x) -> x)
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)
View
@@ -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,103 +54,70 @@ 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}
])
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}
])
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}
])
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)
View
@@ -1,10 +1,11 @@
{module, inject} = angular.mock
Channel = require('jschannel')
RPC = require('frame-rpc')
describe 'Bridge', ->
sandbox = sinon.sandbox.create()
bridge = null
createChannel = null
fakeWindow = null
before ->
angular.module('h', [])
@@ -15,230 +16,175 @@ describe 'Bridge', ->
bridge = _bridge_
createChannel = ->
call: sandbox.stub()
bind: sandbox.stub()
unbind: sandbox.stub()
notify: sandbox.stub()
destroy: sandbox.stub()
bridge.createChannel(fakeWindow, 'http://example.com', 'TOKEN')
sandbox.stub(Channel, 'build')
fakeWindow = {
postMessage: sandbox.stub()
}
sandbox.stub(window, 'addEventListener')
sandbox.stub(window, 'removeEventListener')
afterEach ->
sandbox.restore()
describe '.createChannel', ->
it 'creates a new channel with the provided options', ->
Channel.build.returns(createChannel())
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
assert.called(Channel.build)
assert.calledWith(Channel.build, {
window: 'WINDOW'
origin: 'ORIGIN'
scope: 'TOKEN'
onReady: sinon.match.func
})
channel = createChannel()
assert.equal(channel.src, window)
assert.equal(channel.dst, fakeWindow)
assert.equal(channel.origin, 'http://example.com')
it 'adds the channel to the .links property', ->
channel = createChannel()
Channel.build.returns(channel)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
assert.include(bridge.links, {channel: channel, window: 'WINDOW'})
assert.include(bridge.links, {channel: channel, window: fakeWindow})
it 'registers any existing listeners on the channel', ->
message1 = sandbox.spy()
message2 = sandbox.spy()
bridge.on('message1', message1)
bridge.on('message2', message2)
channel = createChannel()
Channel.build.returns(channel)
bridge.on('message1', sinon.spy())
bridge.on('message2', sinon.spy())
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
assert.called(channel.bind)
assert.calledWith(channel.bind, 'message1', sinon.match.func)
assert.calledWith(channel.bind, 'message2', sinon.match.func)
assert.propertyVal(channel._methods, 'message1', message1)
assert.propertyVal(channel._methods, 'message2', message2)
it 'returns the newly created channel', ->
channel = createChannel()
Channel.build.returns(channel)
ret = bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
assert.equal(ret, channel)
assert.instanceOf(channel, RPC)
describe '.call', ->
it 'forwards the call to every created channel', ->
channel = createChannel()
Channel.build.returns(channel)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.call({method: 'method1', params: 'params1'})
sandbox.stub(channel, 'call')
bridge.call('method1', 'params1')
assert.called(channel.call)
message = channel.call.lastCall.args[0]
assert.equal(message.method, 'method1')
assert.equal(message.params, 'params1')
assert.calledWith(channel.call, 'method1', 'params1')
it 'provides a timeout', ->
it 'provides a timeout', (done) ->
channel = createChannel()
Channel.build.returns(channel)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.call({method: 'method1', params: 'params1'})
sandbox.stub(channel, 'call')
sto = sandbox.stub(window, 'setTimeout').yields()
bridge.call('method1', 'params1', done)
message = channel.call.lastCall.args[0]
assert.isNumber(message.timeout)
it 'calls options.callback when all channels return successfully', ->
it 'calls a callback when all channels return successfully', (done) ->
channel1 = createChannel()
channel2 = createChannel()
channel1.call.yieldsTo('success', 'result1')
channel2.call.yieldsTo('success', 'result2')
callback = sandbox.stub()
Channel.build.returns(channel1)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
Channel.build.returns(channel2)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
channel2 = bridge.createChannel(fakeWindow, 'http://example.com', 'NEKOT')
sandbox.stub(channel1, 'call').yields(null, 'result1')
sandbox.stub(channel2, 'call').yields(null, 'result2')
bridge.call({method: 'method1', params: 'params1', callback: callback})
callback = (err, results) ->
assert.isNull(err)
assert.deepEqual(results, ['result1', 'result2'])
done()
assert.called(callback)
assert.calledWith(callback, null, ['result1', 'result2'])
bridge.call('method1', 'params1', callback)
it 'calls options.callback with an error when one or more channels fail', ->
err = new Error('Uh oh')
it 'calls a callback with an error when a channels fails', (done) ->
error = new Error('Uh oh')
channel1 = createChannel()
channel1.call.yieldsTo('error', err, 'A reason for the error')
channel2 = createChannel()
channel2.call.yieldsTo('success', 'result2')
callback = sandbox.stub()
Channel.build.returns(channel1)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
Channel.build.returns(channel2)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
channel2 = bridge.createChannel(fakeWindow, 'http://example.com', 'NEKOT')
sandbox.stub(channel1, 'call').throws(error)
sandbox.stub(channel2, 'call').yields(null, 'result2')
bridge.call({method: 'method1', params: 'params1', callback: callback})
callback = (err, results) ->
assert.equal(err, error)
done()
assert.called(callback)
assert.calledWith(callback, err)
bridge.call('method1', 'params1', callback)
it 'destroys the channel when a call fails', ->
it 'destroys the channel when a call fails', (done) ->
channel = createChannel()
channel.call.yieldsTo('error', new Error(''), 'A reason for the error')
Channel.build.returns(channel)
sandbox.stub(channel, 'call').throws(new Error(''))
sandbox.stub(channel, 'destroy')
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.call({method: 'method1', params: 'params1', callback: sandbox.stub()})
callback = ->
assert.called(channel.destroy)
done()
assert.called(channel.destroy)
bridge.call('method1', 'params1', callback)
it 'no longer publishes to a channel that has had an errored response', ->
it 'no longer publishes to a channel that has had an error', (done) ->
channel = createChannel()
channel.call.yieldsTo('error', new Error(''), 'A reason for the error')
Channel.build.returns(channel)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.call({method: 'method1', params: 'params1', callback: sandbox.stub()})
bridge.call({method: 'method1', params: 'params1', callback: sandbox.stub()})
assert.calledOnce(channel.call)
it 'treats a timeout as a success with no result', ->
sandbox.stub(channel, 'call').throws(new Error('oeunth'))
bridge.call 'method1', 'params1', ->
assert.calledOnce(channel.call)
bridge.call 'method1', 'params1', ->
assert.calledOnce(channel.call)
done()
it 'treats a timeout as a success with no result', (done) ->
channel = createChannel()
channel.call.yieldsTo('error', 'timeout_error', 'timeout')
Channel.build.returns(channel)
callback = sandbox.stub()
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.call({method: 'method1', params: 'params1', callback: callback})
assert.called(callback)
assert.calledWith(callback, null, [null])
sandbox.stub(channel, 'call')
sto = sandbox.stub(window, 'setTimeout').yields()
bridge.call 'method1', 'params1', (err, res) ->
assert.isNull(err)
assert.deepEqual(res, [null])
done()
it 'returns a promise object', ->
channel = createChannel()
channel.call.yieldsTo('error', 'timeout_error', 'timeout')
Channel.build.returns(channel)
ret = bridge.call({method: 'method1', params: 'params1'})
assert.isFunction(ret.then)
describe '.notify', ->
it 'publishes the message on every created channel', ->
channel = createChannel()
message = {method: 'message1', params: 'params'}
Channel.build.returns(channel)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.notify(message)
assert.called(channel.notify)
assert.calledWith(channel.notify, message)
ret = bridge.call('method1', 'params1')
assert.instanceOf(ret, Promise)
describe '.on', ->
it 'registers an event listener on all created channels', ->
it 'adds a method to the method registry', ->
channel = createChannel()
Channel.build.returns(channel)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.on('message1', sandbox.spy())
assert.isFunction(bridge.channelListeners['message1'])
assert.called(channel.bind)
assert.calledWith(channel.bind, 'message1', sinon.match.func)
it 'only allows one message to be registered per method', ->
it 'only allows registering a method once', ->
bridge.on('message1', sandbox.spy())
assert.throws ->
bridge.on('message1', sandbox.spy())
describe '.off', ->
it 'removes the event listener from the created channels', ->
it 'removes the method from the method registry', ->
channel = createChannel()
Channel.build.returns(channel)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.off('message1', sandbox.spy())
it 'ensures that the event is no longer bound when new channels are created', ->
channel1 = createChannel()
channel2 = createChannel()
Channel.build.returns(channel1)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
bridge.off('message1', sandbox.spy())
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
assert.notCalled(channel2.bind)
bridge.on('message1', sandbox.spy())
bridge.off('message1')
assert.isUndefined(bridge.channelListeners['message1'])
describe '.onConnect', ->
it 'adds a callback that is called when a new channel is connected', ->
it 'adds a callback that is called when a channel is connected', (done) ->
callback = (c, s) ->
assert.strictEqual(c, channel)
assert.strictEqual(s, fakeWindow)
done()
data = {
protocol: 'frame-rpc'
method: 'connect'
arguments: ['TOKEN']
}
event = {
origin: 'http://example.com'
data: data
}
addEventListener.yieldsAsync(event)
bridge.onConnect(callback)
channel = createChannel()
Channel.build.returns(channel)
Channel.build.yieldsTo('onReady', channel)
callback = sandbox.stub()
it 'allows multiple callbacks to be registered', (done) ->
callbackCount = 0
callback = (c, s) ->
assert.strictEqual(c, channel)
assert.strictEqual(s, fakeWindow)
if ++callbackCount is 2 then done()
data = {
protocol: 'frame-rpc'
method: 'connect'
arguments: ['TOKEN']
}
event = {
origin: 'http://example.com'
data: data
}
addEventListener.callsArgWithAsync(1, event)
bridge.onConnect(callback)
bridge.onConnect(callback)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
assert.called(callback)
assert.calledWith(callback, channel)
it 'allows multiple callbacks to be registered', ->
channel = createChannel()
Channel.build.returns(channel)
Channel.build.yieldsTo('onReady', channel)
callback1 = sandbox.stub()
callback2 = sandbox.stub()
bridge.onConnect(callback1)
bridge.onConnect(callback2)
bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN')
assert.called(callback1)
assert.called(callback2)
View
@@ -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)
View
@@ -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,29 +53,29 @@ 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', ->
describe 'on "back" event', ->
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
View
@@ -1,5 +1,5 @@
# Shared helper methods for working with unicode strings
unorm = require('./vendor/unorm')
unorm = require('unorm')
# Unicode combining characters
# from http://xregexp.com/addons/unicode/unicode-categories.js line:30
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View
File renamed without changes.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View
File renamed without changes.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -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
View
@@ -43,11 +43,7 @@
</div>
</div>
<div class="pull-right dropdown group-list"
ng-if="auth.user && feature('groups')"
group-list=""
groups="session.state.groups">
</div>
{{ include_raw("h:templates/client/group_list.html") }}
<!-- Searchbar -->
<div class="simple-search"
@@ -101,9 +97,6 @@
<script type="text/ng-template" id="annotation.html">
{{ include_raw("h:templates/client/annotation.html") }}
</script>
<script type="text/ng-template" id="group_list.html">
{{ include_raw("h:templates/client/group_list.html") }}
</script>
<script type="text/ng-template" id="markdown.html">
{{ include_raw("h:templates/client/markdown.html") }}
</script>
View
@@ -1,12 +1,17 @@
<span role="button" class="dropdown-toggle" data-toggle="dropdown">
Groups
<i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu">
<li ng-repeat="group in groups">
<a ng-href="{{group.url}}" ng-bind="group.name" target="_blank"></a>
</li>
<li>
<a href="/groups/new" target="_blank"><i class="h-icon-add"></i> Create a group</a>
</li>
</ul>
<div ng-controller="GroupListCtrl as ctrl"
class="pull-right dropdown group-list">
<span role="button" class="dropdown-toggle" data-toggle="dropdown">
{{ctrl.focusedGroup().name}}
<i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu">
<li ng-repeat="group in ctrl.groups()">
<a ng-bind="group.name" ng-click="ctrl.focusGroup(group.name)">
</a>
</li>
<li>
<a href="/groups/new" target="_blank"><i class="h-icon-add"></i>
New Group</a>
</li>
</ul>
</div>
View
@@ -7,9 +7,12 @@
"angular": "1.2.28",
"angular-animate": "1.2.28",
"angular-jwt": "0.0.9",
"angular-mocks": "^1.2.28",
"angular-resource": "1.2.28",
"angular-route": "1.2.28",
"angular-sanitize": "1.2.28",
"angulartics": "0.17.2",
"autofill-event": "0.0.1",
"babelify": "^6.1.3",
"base-url": "^1.0.0",
"bootstrap": "3.3.5",
@@ -26,18 +29,19 @@
"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",
"moment": "2.5.0",
"moment-timezone": "0.0.1",
"moment": "^2.10.6",
"moment-timezone": "^0.4.0",
"ng-tags-input": "2.2.0",
"node-iterator-shim": "^1.0.1",
"node-uuid": "^1.4.3",
"raf": "^2.0.4",
"raf": "^3.1.0",
"showdown": "^1.2.1",
"strip-bomify": "^0.1.0",
"uglify-js": "^2.4.14"
"uglify-js": "^2.4.14",
"unorm": "^1.3.3"
},
"devDependencies": {
"chai": "^3.2.0",
@@ -71,7 +75,6 @@
"homepage": "https://github.com/hypothesis/h",
"browserify": {
"transform": [
"strip-bomify",
"coffeeify",
"browserify-ngannotate",
"browserify-shim"
@@ -84,7 +87,7 @@
"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"
"moment": "./node_modules/moment/min/moment-with-locales.js"
},
"browserify-shim": {
"annotator": {
@@ -106,7 +109,6 @@
"depends": [
"jquery"
]
},
"jschannel": "Channel"
}
}
}
View
@@ -0,0 +1,7 @@
#!/bin/sh
exec 2>&1
set -e
cd /src/h
/srv/h/bin/hypothesis-worker conf/production.ini nipsa