| @@ -0,0 +1,261 @@ | ||
| assert = chai.assert | ||
| sinon.assert.expose assert, prefix: null | ||
| describe 'CrossFrameBridge', -> | ||
| sandbox = sinon.sandbox.create() | ||
| createBridge = null | ||
| createChannel = null | ||
| beforeEach module('h') | ||
| beforeEach inject (CrossFrameBridge) -> | ||
| createBridge = (options) -> | ||
| new CrossFrameBridge(options) | ||
| createChannel = -> | ||
| call: sandbox.stub() | ||
| bind: sandbox.stub() | ||
| unbind: sandbox.stub() | ||
| notify: sandbox.stub() | ||
| destroy: sandbox.stub() | ||
| sandbox.stub(Channel, 'build') | ||
| afterEach -> | ||
| sandbox.restore() | ||
| describe '.createChannel', -> | ||
| it 'creates a new channel with the provided options', -> | ||
| Channel.build.returns(createChannel()) | ||
| bridge = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| assert.called(Channel.build) | ||
| assert.calledWith(Channel.build, { | ||
| window: 'WINDOW' | ||
| origin: 'ORIGIN' | ||
| scope: 'crossFrameBridge:TOKEN' | ||
| onReady: sinon.match.func | ||
| }) | ||
| it 'adds the channel to the .links property', -> | ||
| channel = createChannel() | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| assert.include(bridge.links, {channel: channel, window: 'WINDOW'}) | ||
| it 'registers any existing listeners on the channel', -> | ||
| channel = createChannel() | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| 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) | ||
| it 'returns the newly created channel', -> | ||
| channel = createChannel() | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| ret = bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| assert.equal(ret, channel) | ||
| describe '.call', -> | ||
| it 'forwards the call to every created channel', -> | ||
| channel = createChannel() | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.call({method: 'method1', params: 'params1'}) | ||
| assert.called(channel.call) | ||
| message = channel.call.lastCall.args[0] | ||
| assert.equal(message.method, 'method1') | ||
| assert.equal(message.params, 'params1') | ||
| it 'provides a timeout', -> | ||
| channel = createChannel() | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.call({method: 'method1', params: 'params1'}) | ||
| message = channel.call.lastCall.args[0] | ||
| assert.isNumber(message.timeout) | ||
| it 'calls options.callback when all channels return successfully', -> | ||
| channel1 = createChannel() | ||
| channel2 = createChannel() | ||
| channel1.call.yieldsTo('success', 'result1') | ||
| channel2.call.yieldsTo('success', 'result2') | ||
| callback = sandbox.stub() | ||
| bridge = createBridge() | ||
| Channel.build.returns(channel1) | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| Channel.build.returns(channel2) | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.call({method: 'method1', params: 'params1', callback: callback}) | ||
| assert.called(callback) | ||
| assert.calledWith(callback, null, ['result1', 'result2']) | ||
| it 'calls options.callback with an error when one or more channels fail', -> | ||
| err = 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() | ||
| bridge = createBridge() | ||
| Channel.build.returns(channel1) | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| Channel.build.returns(channel2) | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.call({method: 'method1', params: 'params1', callback: callback}) | ||
| assert.called(callback) | ||
| assert.calledWith(callback, err) | ||
| it 'destroys the channel when a call fails', -> | ||
| channel = createChannel() | ||
| channel.call.yieldsTo('error', new Error(''), 'A reason for the error') | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.call({method: 'method1', params: 'params1', callback: sandbox.stub()}) | ||
| assert.called(channel.destroy) | ||
| it 'no longer publishes to a channel that has had an errored response', -> | ||
| channel = createChannel() | ||
| channel.call.yieldsTo('error', new Error(''), 'A reason for the error') | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| 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', -> | ||
| channel = createChannel() | ||
| channel.call.yieldsTo('error', 'timeout_error', 'timeout') | ||
| Channel.build.returns(channel) | ||
| callback = sandbox.stub() | ||
| bridge = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.call({method: 'method1', params: 'params1', callback: callback}) | ||
| assert.called(callback) | ||
| assert.calledWith(callback, null, [null]) | ||
| it 'returns a promise object', -> | ||
| channel = createChannel() | ||
| channel.call.yieldsTo('error', 'timeout_error', 'timeout') | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| 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 = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.notify(message) | ||
| assert.called(channel.notify) | ||
| assert.calledWith(channel.notify, message) | ||
| describe '.on', -> | ||
| it 'registers an event listener on all created channels', -> | ||
| channel = createChannel() | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.on('message1', sandbox.spy()) | ||
| assert.called(channel.bind) | ||
| assert.calledWith(channel.bind, 'message1', sinon.match.func) | ||
| it 'only allows one message to be registered per method', -> | ||
| bridge = createBridge() | ||
| bridge.on('message1', sandbox.spy()) | ||
| assert.throws -> | ||
| bridge.on('message1', sandbox.spy()) | ||
| describe '.off', -> | ||
| it 'removes the event listener from the created channels', -> | ||
| channel = createChannel() | ||
| Channel.build.returns(channel) | ||
| bridge = createBridge() | ||
| 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 = createBridge() | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| bridge.off('message1', sandbox.spy()) | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| assert.notCalled(channel2.bind) | ||
| describe '.onConnect', -> | ||
| it 'adds a callback that is called when a new channel is connected', -> | ||
| channel = createChannel() | ||
| Channel.build.returns(channel) | ||
| Channel.build.yieldsTo('onReady', channel) | ||
| callback = sandbox.stub() | ||
| bridge = createBridge() | ||
| 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 = createBridge() | ||
| bridge.onConnect(callback1) | ||
| bridge.onConnect(callback2) | ||
| bridge.createChannel('WINDOW', 'ORIGIN', 'TOKEN') | ||
| assert.called(callback1) | ||
| assert.called(callback2) | ||
| @@ -0,0 +1,188 @@ | ||
| assert = chai.assert | ||
| sinon.assert.expose assert, prefix: null | ||
| describe 'CrossFrameDiscovery', -> | ||
| sandbox = sinon.sandbox.create() | ||
| fakeTopWindow = null | ||
| fakeFrameWindow = null | ||
| createDiscovery = null | ||
| beforeEach module('h') | ||
| beforeEach inject (CrossFrameDiscovery) -> | ||
| createDiscovery = (win, options) -> | ||
| new CrossFrameDiscovery(win, options) | ||
| createWindow = -> | ||
| top: null | ||
| addEventListener: sandbox.stub() | ||
| removeEventListener: sandbox.stub() | ||
| postMessage: sandbox.stub() | ||
| length: 0 | ||
| frames: [] | ||
| fakeTopWindow = createWindow() | ||
| fakeTopWindow.top = fakeTopWindow | ||
| fakeFrameWindow = createWindow() | ||
| fakeFrameWindow.top = fakeTopWindow | ||
| fakeTopWindow.frames = [fakeFrameWindow] | ||
| afterEach -> | ||
| sandbox.restore() | ||
| describe 'startDiscovery', -> | ||
| it 'adds a "message" listener to the window object', -> | ||
| discovery = createDiscovery(fakeTopWindow) | ||
| discovery.startDiscovery(->) | ||
| assert.called(fakeTopWindow.addEventListener) | ||
| assert.calledWith(fakeTopWindow.addEventListener, 'message', sinon.match.func, false) | ||
| describe 'when acting as a server (options.server = true)', -> | ||
| server = null | ||
| beforeEach -> | ||
| server = createDiscovery(fakeFrameWindow, server: true) | ||
| it 'sends out a "offer" message to every frame', -> | ||
| server.startDiscovery(->) | ||
| assert.called(fakeTopWindow.postMessage) | ||
| assert.calledWith(fakeTopWindow.postMessage, '__cross_frame_dhcp_offer', '*') | ||
| it 'allows the origin to be provided', -> | ||
| server = createDiscovery(fakeFrameWindow, server: true, origin: 'foo') | ||
| server.startDiscovery(->) | ||
| assert.called(fakeTopWindow.postMessage) | ||
| assert.calledWith(fakeTopWindow.postMessage, '__cross_frame_dhcp_offer', 'foo') | ||
| it 'does not send the message to itself', -> | ||
| server.startDiscovery(->) | ||
| assert.notCalled(fakeFrameWindow.postMessage) | ||
| it 'sends an "ack" on receiving a "request"', -> | ||
| fakeFrameWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_request' | ||
| source: fakeTopWindow | ||
| origin: 'top' | ||
| }) | ||
| server.startDiscovery(->) | ||
| assert.called(fakeTopWindow.postMessage) | ||
| matcher = sinon.match(/__cross_frame_dhcp_ack:\d+/) | ||
| assert.calledWith(fakeTopWindow.postMessage, matcher, 'top') | ||
| it 'calls the discovery callback on receiving "request"', -> | ||
| fakeFrameWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_request' | ||
| source: fakeTopWindow | ||
| origin: 'top' | ||
| }) | ||
| handler = sandbox.stub() | ||
| server.startDiscovery(handler) | ||
| assert.called(handler) | ||
| assert.calledWith(handler, fakeTopWindow, 'top', sinon.match(/\d+/)) | ||
| it 'raises an error if it recieves an event from another server', -> | ||
| fakeFrameWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_offer' | ||
| source: fakeTopWindow | ||
| origin: 'top' | ||
| }) | ||
| handler = sandbox.stub() | ||
| assert.throws -> | ||
| server.startDiscovery(handler) | ||
| describe 'when acting as a client (options.client = false)', -> | ||
| client = null | ||
| beforeEach -> | ||
| client = createDiscovery(fakeTopWindow) | ||
| it 'sends out a discovery message to every frame', -> | ||
| client.startDiscovery(->) | ||
| assert.called(fakeFrameWindow.postMessage) | ||
| assert.calledWith(fakeFrameWindow.postMessage, '__cross_frame_dhcp_discovery', '*') | ||
| it 'does not send the message to itself', -> | ||
| client.startDiscovery(->) | ||
| assert.notCalled(fakeTopWindow.postMessage) | ||
| it 'sends a "request" in response to an "offer"', -> | ||
| fakeTopWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_offer' | ||
| source: fakeFrameWindow | ||
| origin: 'iframe' | ||
| }) | ||
| client.startDiscovery(->) | ||
| assert.called(fakeFrameWindow.postMessage) | ||
| assert.calledWith(fakeFrameWindow.postMessage, '__cross_frame_dhcp_request', 'iframe') | ||
| it 'does not respond to an "offer" if a "request" is already in progress', -> | ||
| fakeTopWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_offer' | ||
| source: fakeFrameWindow | ||
| origin: 'iframe1' | ||
| }) | ||
| fakeTopWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_offer' | ||
| source: fakeFrameWindow | ||
| origin: 'iframe2' | ||
| }) | ||
| client.startDiscovery(->) | ||
| # Twice, once for discovery, once for offer. | ||
| assert.calledTwice(fakeFrameWindow.postMessage) | ||
| lastCall = fakeFrameWindow.postMessage.lastCall | ||
| assert(lastCall.notCalledWith(sinon.match.string, 'iframe2')) | ||
| it 'allows responding to a "request" once a previous "request" has completed', -> | ||
| fakeTopWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_offer' | ||
| source: fakeFrameWindow | ||
| origin: 'iframe1' | ||
| }) | ||
| fakeTopWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_ack:1234' | ||
| source: fakeFrameWindow | ||
| origin: 'iframe1' | ||
| }) | ||
| fakeTopWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_offer' | ||
| source: fakeFrameWindow | ||
| origin: 'iframe2' | ||
| }) | ||
| client.startDiscovery(->) | ||
| assert.called(fakeFrameWindow.postMessage) | ||
| assert.calledWith(fakeFrameWindow.postMessage, '__cross_frame_dhcp_request', 'iframe2') | ||
| it 'calls the discovery callback on receiving an "ack"', -> | ||
| fakeTopWindow.addEventListener.yields({ | ||
| data: '__cross_frame_dhcp_ack:1234' | ||
| source: fakeFrameWindow | ||
| origin: 'iframe' | ||
| }) | ||
| callback = sandbox.stub() | ||
| client.startDiscovery(callback) | ||
| assert.called(callback) | ||
| assert.calledWith(callback, fakeFrameWindow, 'iframe', '1234') | ||
| describe 'stopDiscovery', -> | ||
| it 'removes the "message" listener from the window', -> | ||
| discovery = createDiscovery(fakeFrameWindow) | ||
| discovery.startDiscovery() | ||
| discovery.stopDiscovery() | ||
| handler = fakeFrameWindow.addEventListener.lastCall.args[1] | ||
| assert.called(fakeFrameWindow.removeEventListener) | ||
| assert.calledWith(fakeFrameWindow.removeEventListener, 'message', handler) | ||
| it 'allows startDiscovery to be called with a new handler', -> | ||
| discovery = createDiscovery(fakeFrameWindow) | ||
| discovery.startDiscovery() | ||
| discovery.stopDiscovery() | ||
| assert.doesNotThrow -> | ||
| discovery.startDiscovery() |
| @@ -0,0 +1,86 @@ | ||
| assert = chai.assert | ||
| sinon.assert.expose assert, prefix: null | ||
| describe 'CrossFrameService', -> | ||
| sandbox = sinon.sandbox.create() | ||
| crossframe = null | ||
| $rootScope = null | ||
| $fakeDocument = null | ||
| $fakeWindow = null | ||
| fakeStore = null | ||
| fakeAnnotationUI = null | ||
| fakeCrossFrameDiscovery = null | ||
| fakeCrossFrameBridge = null | ||
| fakeAnnotationSync = null | ||
| fakeAnnotationUISync = null | ||
| beforeEach module('h') | ||
| beforeEach module ($provide) -> | ||
| $fakeDocument = {} | ||
| $fakeWindow = {} | ||
| fakeStore = {} | ||
| fakeAnnotationUI = {} | ||
| fakeCrossFrameDiscovery = | ||
| startDiscovery: sandbox.stub() | ||
| fakeCrossFrameBridge = | ||
| notify: sandbox.stub() | ||
| createChannel: sandbox.stub() | ||
| onConnect: sandbox.stub() | ||
| fakeAnnotationSync = {} | ||
| fakeAnnotationUISync = {} | ||
| $provide.value('$document', $fakeDocument) | ||
| $provide.value('$window', $fakeWindow) | ||
| $provide.value('store', fakeStore) | ||
| $provide.value('annotationUI', fakeAnnotationUI) | ||
| $provide.value('CrossFrameDiscovery', | ||
| sandbox.stub().returns(fakeCrossFrameDiscovery)) | ||
| $provide.value('CrossFrameBridge', | ||
| sandbox.stub().returns(fakeCrossFrameBridge)) | ||
| $provide.value('AnnotationSync', | ||
| sandbox.stub().returns(fakeAnnotationSync)) | ||
| $provide.value('AnnotationUISync', | ||
| sandbox.stub().returns(fakeAnnotationUISync)) | ||
| return # $provide returns a promise. | ||
| beforeEach inject (_$rootScope_, _crossframe_) -> | ||
| $rootScope = _$rootScope_ | ||
| crossframe = _crossframe_ | ||
| afterEach -> | ||
| sandbox.restore() | ||
| describe '.connect()', -> | ||
| it 'creates a new channel when the discovery module finds a frame', -> | ||
| fakeCrossFrameDiscovery.startDiscovery.yields('source', 'origin', 'token') | ||
| crossframe.connect() | ||
| assert.calledWith(fakeCrossFrameBridge.createChannel, | ||
| 'source', 'origin', 'token') | ||
| it 'queries discovered frames for metadata', -> | ||
| info = {metadata: link: [{href: 'http://example.com'}]} | ||
| channel = {call: sandbox.stub().yieldsTo('success', info)} | ||
| fakeCrossFrameBridge.onConnect.yields(channel) | ||
| crossframe.connect() | ||
| assert.calledWith(channel.call, { | ||
| method: 'getDocumentInfo' | ||
| success: sinon.match.func | ||
| }) | ||
| it 'updates the providers array', -> | ||
| info = {metadata: link: [{href: 'http://example.com'}]} | ||
| channel = {call: sandbox.stub().yieldsTo('success', info)} | ||
| fakeCrossFrameBridge.onConnect.yields(channel) | ||
| crossframe.connect() | ||
| assert.deepEqual(crossframe.providers, [ | ||
| {channel: channel, entities: ['http://example.com']} | ||
| ]) | ||
| describe '.notify()', -> | ||
| it 'proxies the call to the bridge', -> | ||
| message = {method: 'foo', params: 'bar'} | ||
| crossframe.connect() # create the bridge. | ||
| crossframe.notify(message) | ||
| assert.calledOn(fakeCrossFrameBridge.notify, fakeCrossFrameBridge) | ||
| assert.calledWith(fakeCrossFrameBridge.notify, message) |
| @@ -0,0 +1,84 @@ | ||
| assert = chai.assert | ||
| sinon.assert.expose(assert, prefix: '') | ||
| describe 'Threading', -> | ||
| instance = null | ||
| beforeEach module('h') | ||
| beforeEach inject (_threading_) -> | ||
| instance = _threading_ | ||
| describe 'pruneEmpties', -> | ||
| it 'keeps public messages with no children', -> | ||
| threadA = mail.messageContainer(mail.message('subject a', 'a', [])) | ||
| threadB = mail.messageContainer(mail.message('subject b', 'b', [])) | ||
| threadC = mail.messageContainer(mail.message('subject c', 'c', [])) | ||
| root = mail.messageContainer() | ||
| root.addChild(threadA) | ||
| root.addChild(threadB) | ||
| root.addChild(threadC) | ||
| instance.pruneEmpties(root) | ||
| assert.equal(root.children.length, 3) | ||
| it 'keeps public messages with public children', -> | ||
| threadA = mail.messageContainer(mail.message('subject a', 'a', [])) | ||
| threadA1 = mail.messageContainer(mail.message('subject a1', 'a1', ['a'])) | ||
| threadA2 = mail.messageContainer(mail.message('subject a2', 'a2', ['a'])) | ||
| root = mail.messageContainer() | ||
| root.addChild(threadA) | ||
| threadA.addChild(threadA1) | ||
| threadA.addChild(threadA2) | ||
| instance.pruneEmpties(root) | ||
| assert.equal(root.children.length, 1) | ||
| it 'prunes private messages with no children', -> | ||
| threadA = mail.messageContainer() | ||
| threadB = mail.messageContainer() | ||
| threadC = mail.messageContainer() | ||
| root = mail.messageContainer() | ||
| root.addChild(threadA) | ||
| root.addChild(threadB) | ||
| root.addChild(threadC) | ||
| instance.pruneEmpties(root) | ||
| assert.equal(root.children.length, 0) | ||
| it 'keeps private messages with public children', -> | ||
| threadA = mail.messageContainer() | ||
| threadA1 = mail.messageContainer(mail.message('subject a1', 'a1', ['a'])) | ||
| threadA2 = mail.messageContainer(mail.message('subject a2', 'a2', ['a'])) | ||
| root = mail.messageContainer() | ||
| root.addChild(threadA) | ||
| threadA.addChild(threadA1) | ||
| threadA.addChild(threadA2) | ||
| instance.pruneEmpties(root) | ||
| assert.equal(root.children.length, 1) | ||
| it 'prunes private messages with private children', -> | ||
| threadA = mail.messageContainer() | ||
| threadA1 = mail.messageContainer() | ||
| threadA2 = mail.messageContainer() | ||
| root = mail.messageContainer() | ||
| root.addChild(threadA) | ||
| threadA.addChild(threadA1) | ||
| threadA.addChild(threadA2) | ||
| instance.pruneEmpties(root) | ||
| assert.equal(root.children.length, 0) |