mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-19 23:17:16 +03:00
483 lines
17 KiB
JavaScript
483 lines
17 KiB
JavaScript
const CommandRegistry = require('../src/command-registry')
|
|
const _ = require('underscore-plus')
|
|
|
|
describe('CommandRegistry', () => {
|
|
let registry, parent, child, grandchild
|
|
|
|
beforeEach(() => {
|
|
parent = document.createElement('div')
|
|
child = document.createElement('div')
|
|
grandchild = document.createElement('div')
|
|
parent.classList.add('parent')
|
|
child.classList.add('child')
|
|
grandchild.classList.add('grandchild')
|
|
child.appendChild(grandchild)
|
|
parent.appendChild(child)
|
|
document.querySelector('#jasmine-content').appendChild(parent)
|
|
|
|
registry = new CommandRegistry()
|
|
registry.attach(parent)
|
|
})
|
|
|
|
afterEach(() => registry.destroy())
|
|
|
|
describe('when a command event is dispatched on an element', () => {
|
|
it('invokes callbacks with selectors matching the target', () => {
|
|
let called = false
|
|
registry.add('.grandchild', 'command', function (event) {
|
|
expect(this).toBe(grandchild)
|
|
expect(event.type).toBe('command')
|
|
expect(event.eventPhase).toBe(Event.BUBBLING_PHASE)
|
|
expect(event.target).toBe(grandchild)
|
|
expect(event.currentTarget).toBe(grandchild)
|
|
called = true
|
|
})
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true }))
|
|
expect(called).toBe(true)
|
|
})
|
|
|
|
it('invokes callbacks with selectors matching ancestors of the target', () => {
|
|
const calls = []
|
|
|
|
registry.add('.child', 'command', function (event) {
|
|
expect(this).toBe(child)
|
|
expect(event.target).toBe(grandchild)
|
|
expect(event.currentTarget).toBe(child)
|
|
calls.push('child')
|
|
})
|
|
|
|
registry.add('.parent', 'command', function (event) {
|
|
expect(this).toBe(parent)
|
|
expect(event.target).toBe(grandchild)
|
|
expect(event.currentTarget).toBe(parent)
|
|
calls.push('parent')
|
|
})
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true }))
|
|
expect(calls).toEqual(['child', 'parent'])
|
|
})
|
|
|
|
it('invokes inline listeners prior to listeners applied via selectors', () => {
|
|
const calls = []
|
|
registry.add('.grandchild', 'command', () => calls.push('grandchild'))
|
|
registry.add(child, 'command', () => calls.push('child-inline'))
|
|
registry.add('.child', 'command', () => calls.push('child'))
|
|
registry.add('.parent', 'command', () => calls.push('parent'))
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true }))
|
|
expect(calls).toEqual(['grandchild', 'child-inline', 'child', 'parent'])
|
|
})
|
|
|
|
it('orders multiple matching listeners for an element by selector specificity', () => {
|
|
child.classList.add('foo', 'bar')
|
|
const calls = []
|
|
|
|
registry.add('.foo.bar', 'command', () => calls.push('.foo.bar'))
|
|
registry.add('.foo', 'command', () => calls.push('.foo'))
|
|
registry.add('.bar', 'command', () => calls.push('.bar')) // specificity ties favor commands added later, like CSS
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true }))
|
|
expect(calls).toEqual(['.foo.bar', '.bar', '.foo'])
|
|
})
|
|
|
|
it('orders inline listeners by reverse registration order', () => {
|
|
const calls = []
|
|
registry.add(child, 'command', () => calls.push('child1'))
|
|
registry.add(child, 'command', () => calls.push('child2'))
|
|
child.dispatchEvent(new CustomEvent('command', { bubbles: true }))
|
|
expect(calls).toEqual(['child2', 'child1'])
|
|
})
|
|
|
|
it('stops bubbling through ancestors when .stopPropagation() is called on the event', () => {
|
|
const calls = []
|
|
|
|
registry.add('.parent', 'command', () => calls.push('parent'))
|
|
registry.add('.child', 'command', () => calls.push('child-2'))
|
|
registry.add('.child', 'command', event => {
|
|
calls.push('child-1')
|
|
event.stopPropagation()
|
|
})
|
|
|
|
const dispatchedEvent = new CustomEvent('command', { bubbles: true })
|
|
spyOn(dispatchedEvent, 'stopPropagation')
|
|
grandchild.dispatchEvent(dispatchedEvent)
|
|
expect(calls).toEqual(['child-1', 'child-2'])
|
|
expect(dispatchedEvent.stopPropagation).toHaveBeenCalled()
|
|
})
|
|
|
|
it('stops invoking callbacks when .stopImmediatePropagation() is called on the event', () => {
|
|
const calls = []
|
|
|
|
registry.add('.parent', 'command', () => calls.push('parent'))
|
|
registry.add('.child', 'command', () => calls.push('child-2'))
|
|
registry.add('.child', 'command', event => {
|
|
calls.push('child-1')
|
|
event.stopImmediatePropagation()
|
|
})
|
|
|
|
const dispatchedEvent = new CustomEvent('command', { bubbles: true })
|
|
spyOn(dispatchedEvent, 'stopImmediatePropagation')
|
|
grandchild.dispatchEvent(dispatchedEvent)
|
|
expect(calls).toEqual(['child-1'])
|
|
expect(dispatchedEvent.stopImmediatePropagation).toHaveBeenCalled()
|
|
})
|
|
|
|
it('forwards .preventDefault() calls from the synthetic event to the original', () => {
|
|
registry.add('.child', 'command', event => event.preventDefault())
|
|
|
|
const dispatchedEvent = new CustomEvent('command', { bubbles: true })
|
|
spyOn(dispatchedEvent, 'preventDefault')
|
|
grandchild.dispatchEvent(dispatchedEvent)
|
|
expect(dispatchedEvent.preventDefault).toHaveBeenCalled()
|
|
})
|
|
|
|
it('forwards .abortKeyBinding() calls from the synthetic event to the original', () => {
|
|
registry.add('.child', 'command', event => event.abortKeyBinding())
|
|
|
|
const dispatchedEvent = new CustomEvent('command', { bubbles: true })
|
|
dispatchedEvent.abortKeyBinding = jasmine.createSpy('abortKeyBinding')
|
|
grandchild.dispatchEvent(dispatchedEvent)
|
|
expect(dispatchedEvent.abortKeyBinding).toHaveBeenCalled()
|
|
})
|
|
|
|
it('copies non-standard properties from the original event to the synthetic event', () => {
|
|
let syntheticEvent = null
|
|
registry.add('.child', 'command', event => (syntheticEvent = event))
|
|
|
|
const dispatchedEvent = new CustomEvent('command', { bubbles: true })
|
|
dispatchedEvent.nonStandardProperty = 'testing'
|
|
grandchild.dispatchEvent(dispatchedEvent)
|
|
expect(syntheticEvent.nonStandardProperty).toBe('testing')
|
|
})
|
|
|
|
it('allows listeners to be removed via a disposable returned by ::add', () => {
|
|
let calls = []
|
|
|
|
const disposable1 = registry.add('.parent', 'command', () =>
|
|
calls.push('parent')
|
|
)
|
|
const disposable2 = registry.add('.child', 'command', () =>
|
|
calls.push('child')
|
|
)
|
|
|
|
disposable1.dispose()
|
|
grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true }))
|
|
expect(calls).toEqual(['child'])
|
|
|
|
calls = []
|
|
disposable2.dispose()
|
|
grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true }))
|
|
expect(calls).toEqual([])
|
|
})
|
|
|
|
it('allows multiple commands to be registered under one selector when called with an object', () => {
|
|
let calls = []
|
|
|
|
const disposable = registry.add('.child', {
|
|
'command-1' () {
|
|
calls.push('command-1')
|
|
},
|
|
'command-2' () {
|
|
calls.push('command-2')
|
|
}
|
|
})
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command-1', { bubbles: true }))
|
|
grandchild.dispatchEvent(new CustomEvent('command-2', { bubbles: true }))
|
|
|
|
expect(calls).toEqual(['command-1', 'command-2'])
|
|
|
|
calls = []
|
|
disposable.dispose()
|
|
grandchild.dispatchEvent(new CustomEvent('command-1', { bubbles: true }))
|
|
grandchild.dispatchEvent(new CustomEvent('command-2', { bubbles: true }))
|
|
expect(calls).toEqual([])
|
|
})
|
|
|
|
it('invokes callbacks registered with ::onWillDispatch and ::onDidDispatch', () => {
|
|
const sequence = []
|
|
|
|
registry.onDidDispatch(event => sequence.push(['onDidDispatch', event]))
|
|
|
|
registry.add('.grandchild', 'command', event =>
|
|
sequence.push(['listener', event])
|
|
)
|
|
|
|
registry.onWillDispatch(event => sequence.push(['onWillDispatch', event]))
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true }))
|
|
|
|
expect(sequence[0][0]).toBe('onWillDispatch')
|
|
expect(sequence[1][0]).toBe('listener')
|
|
expect(sequence[2][0]).toBe('onDidDispatch')
|
|
|
|
expect(
|
|
sequence[0][1] === sequence[1][1] && sequence[1][1] === sequence[2][1]
|
|
).toBe(true)
|
|
expect(sequence[0][1].constructor).toBe(CustomEvent)
|
|
expect(sequence[0][1].target).toBe(grandchild)
|
|
})
|
|
})
|
|
|
|
describe('::add(selector, commandName, callback)', () => {
|
|
it('throws an error when called with an invalid selector', () => {
|
|
const badSelector = '<>'
|
|
let addError = null
|
|
try {
|
|
registry.add(badSelector, 'foo:bar', () => {})
|
|
} catch (error) {
|
|
addError = error
|
|
}
|
|
expect(addError.message).toContain(badSelector)
|
|
})
|
|
|
|
it('throws an error when called with a null callback and selector target', () => {
|
|
const badCallback = null
|
|
|
|
expect(() => {
|
|
registry.add('.selector', 'foo:bar', badCallback)
|
|
}).toThrow(new Error('Cannot register a command with a null listener.'))
|
|
})
|
|
|
|
it('throws an error when called with a null callback and object target', () => {
|
|
const badCallback = null
|
|
|
|
expect(() => {
|
|
registry.add(document.body, 'foo:bar', badCallback)
|
|
}).toThrow(new Error('Cannot register a command with a null listener.'))
|
|
})
|
|
|
|
it('throws an error when called with an object listener without a didDispatch method', () => {
|
|
const badListener = {
|
|
title: 'a listener without a didDispatch callback',
|
|
description: 'this should throw an error'
|
|
}
|
|
|
|
expect(() => {
|
|
registry.add(document.body, 'foo:bar', badListener)
|
|
}).toThrow(
|
|
new Error(
|
|
'Listener must be a callback function or an object with a didDispatch method.'
|
|
)
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('::findCommands({target})', () => {
|
|
it('returns command descriptors that can be invoked on the target or its ancestors', () => {
|
|
registry.add('.parent', 'namespace:command-1', () => {})
|
|
registry.add('.child', 'namespace:command-2', () => {})
|
|
registry.add('.grandchild', 'namespace:command-3', () => {})
|
|
registry.add('.grandchild.no-match', 'namespace:command-4', () => {})
|
|
|
|
registry.add(grandchild, 'namespace:inline-command-1', () => {})
|
|
registry.add(child, 'namespace:inline-command-2', () => {})
|
|
|
|
const commands = registry.findCommands({ target: grandchild })
|
|
const nonJqueryCommands = _.reject(commands, cmd => cmd.jQuery)
|
|
expect(nonJqueryCommands).toEqual([
|
|
{
|
|
name: 'namespace:inline-command-1',
|
|
displayName: 'Namespace: Inline Command 1'
|
|
},
|
|
{ name: 'namespace:command-3', displayName: 'Namespace: Command 3' },
|
|
{
|
|
name: 'namespace:inline-command-2',
|
|
displayName: 'Namespace: Inline Command 2'
|
|
},
|
|
{ name: 'namespace:command-2', displayName: 'Namespace: Command 2' },
|
|
{ name: 'namespace:command-1', displayName: 'Namespace: Command 1' }
|
|
])
|
|
})
|
|
|
|
it('returns command descriptors with arbitrary metadata if set in a listener object', () => {
|
|
registry.add('.grandchild', 'namespace:command-1', () => {})
|
|
registry.add('.grandchild', 'namespace:command-2', {
|
|
displayName: 'Custom Command 2',
|
|
metadata: {
|
|
some: 'other',
|
|
object: 'data'
|
|
},
|
|
didDispatch () {}
|
|
})
|
|
registry.add('.grandchild', 'namespace:command-3', {
|
|
name: 'some:other:incorrect:commandname',
|
|
displayName: 'Custom Command 3',
|
|
metadata: {
|
|
some: 'other',
|
|
object: 'data'
|
|
},
|
|
didDispatch () {}
|
|
})
|
|
|
|
const commands = registry.findCommands({ target: grandchild })
|
|
expect(commands).toEqual([
|
|
{
|
|
displayName: 'Namespace: Command 1',
|
|
name: 'namespace:command-1'
|
|
},
|
|
{
|
|
displayName: 'Custom Command 2',
|
|
metadata: {
|
|
some: 'other',
|
|
object: 'data'
|
|
},
|
|
name: 'namespace:command-2'
|
|
},
|
|
{
|
|
displayName: 'Custom Command 3',
|
|
metadata: {
|
|
some: 'other',
|
|
object: 'data'
|
|
},
|
|
name: 'namespace:command-3'
|
|
}
|
|
])
|
|
})
|
|
|
|
it('returns command descriptors with arbitrary metadata if set on a listener function', () => {
|
|
function listener () {}
|
|
listener.displayName = 'Custom Command 2'
|
|
listener.metadata = {
|
|
some: 'other',
|
|
object: 'data'
|
|
}
|
|
|
|
registry.add('.grandchild', 'namespace:command-2', listener)
|
|
const commands = registry.findCommands({ target: grandchild })
|
|
expect(commands).toEqual([
|
|
{
|
|
displayName: 'Custom Command 2',
|
|
metadata: {
|
|
some: 'other',
|
|
object: 'data'
|
|
},
|
|
name: 'namespace:command-2'
|
|
}
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('::dispatch(target, commandName)', () => {
|
|
it('simulates invocation of the given command ', () => {
|
|
let called = false
|
|
registry.add('.grandchild', 'command', function (event) {
|
|
expect(this).toBe(grandchild)
|
|
expect(event.type).toBe('command')
|
|
expect(event.eventPhase).toBe(Event.BUBBLING_PHASE)
|
|
expect(event.target).toBe(grandchild)
|
|
expect(event.currentTarget).toBe(grandchild)
|
|
called = true
|
|
})
|
|
|
|
registry.dispatch(grandchild, 'command')
|
|
expect(called).toBe(true)
|
|
})
|
|
|
|
it('returns a promise if any listeners matched the command', () => {
|
|
registry.add('.grandchild', 'command', () => {})
|
|
|
|
expect(registry.dispatch(grandchild, 'command').constructor.name).toBe(
|
|
'Promise'
|
|
)
|
|
expect(registry.dispatch(grandchild, 'bogus')).toBe(null)
|
|
expect(registry.dispatch(parent, 'command')).toBe(null)
|
|
})
|
|
|
|
it('returns a promise that resolves when the listeners resolve', async () => {
|
|
jasmine.useRealClock()
|
|
registry.add('.grandchild', 'command', () => 1)
|
|
registry.add('.grandchild', 'command', () => Promise.resolve(2))
|
|
registry.add(
|
|
'.grandchild',
|
|
'command',
|
|
() =>
|
|
new Promise(resolve => {
|
|
setTimeout(() => {
|
|
resolve(3)
|
|
}, 1)
|
|
})
|
|
)
|
|
|
|
const values = await registry.dispatch(grandchild, 'command')
|
|
expect(values).toEqual([3, 2, 1])
|
|
})
|
|
|
|
it('returns a promise that rejects when a listener is rejected', async () => {
|
|
jasmine.useRealClock()
|
|
registry.add('.grandchild', 'command', () => 1)
|
|
registry.add('.grandchild', 'command', () => Promise.resolve(2))
|
|
registry.add(
|
|
'.grandchild',
|
|
'command',
|
|
() =>
|
|
new Promise((resolve, reject) => {
|
|
setTimeout(() => {
|
|
reject(3)
|
|
}, 1)
|
|
})
|
|
)
|
|
|
|
let value
|
|
try {
|
|
value = await registry.dispatch(grandchild, 'command')
|
|
} catch (err) {
|
|
value = err
|
|
}
|
|
expect(value).toBe(3)
|
|
})
|
|
})
|
|
|
|
describe('::getSnapshot and ::restoreSnapshot', () =>
|
|
it('removes all command handlers except for those in the snapshot', () => {
|
|
registry.add('.parent', 'namespace:command-1', () => {})
|
|
registry.add('.child', 'namespace:command-2', () => {})
|
|
const snapshot = registry.getSnapshot()
|
|
registry.add('.grandchild', 'namespace:command-3', () => {})
|
|
|
|
expect(registry.findCommands({ target: grandchild }).slice(0, 3)).toEqual(
|
|
[
|
|
{ name: 'namespace:command-3', displayName: 'Namespace: Command 3' },
|
|
{ name: 'namespace:command-2', displayName: 'Namespace: Command 2' },
|
|
{ name: 'namespace:command-1', displayName: 'Namespace: Command 1' }
|
|
]
|
|
)
|
|
|
|
registry.restoreSnapshot(snapshot)
|
|
|
|
expect(registry.findCommands({ target: grandchild }).slice(0, 2)).toEqual(
|
|
[
|
|
{ name: 'namespace:command-2', displayName: 'Namespace: Command 2' },
|
|
{ name: 'namespace:command-1', displayName: 'Namespace: Command 1' }
|
|
]
|
|
)
|
|
|
|
registry.add('.grandchild', 'namespace:command-3', () => {})
|
|
registry.restoreSnapshot(snapshot)
|
|
|
|
expect(registry.findCommands({ target: grandchild }).slice(0, 2)).toEqual(
|
|
[
|
|
{ name: 'namespace:command-2', displayName: 'Namespace: Command 2' },
|
|
{ name: 'namespace:command-1', displayName: 'Namespace: Command 1' }
|
|
]
|
|
)
|
|
}))
|
|
|
|
describe('::attach(rootNode)', () =>
|
|
it('adds event listeners for any previously-added commands', () => {
|
|
const registry2 = new CommandRegistry()
|
|
|
|
const commandSpy = jasmine.createSpy('command-callback')
|
|
registry2.add('.grandchild', 'command-1', commandSpy)
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command-1', { bubbles: true }))
|
|
expect(commandSpy).not.toHaveBeenCalled()
|
|
|
|
registry2.attach(parent)
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command-1', { bubbles: true }))
|
|
expect(commandSpy).toHaveBeenCalled()
|
|
}))
|
|
})
|