pulsar/spec/context-menu-manager-spec.coffee
Jordan Eldredge 228f65da5f Base context menu accelerators on activeElement
Addresses issue pointed by out @nathansobo in #15277 where keybindings
for unfocusable nodes were being surfaced as accelerator indicators in
context menus.

When you right click in the DOM, your focus goes to the first focusable
ancestor of your click target. This change uses the ancestor that you
are actually focused on when looking for avaliable key bindings rather
than using the event target directly. This ensures that any surfaced key
bindings are actually reachable.
2017-08-16 11:58:05 -07:00

336 lines
10 KiB
CoffeeScript

ContextMenuManager = require '../src/context-menu-manager'
describe "ContextMenuManager", ->
[contextMenu, parent, child, grandchild] = []
beforeEach ->
{resourcePath} = atom.getLoadSettings()
contextMenu = new ContextMenuManager({keymapManager: atom.keymaps})
contextMenu.initialize({resourcePath})
parent = document.createElement("div")
child = document.createElement("div")
grandchild = document.createElement("div")
parent.tabIndex = -1
child.tabIndex = -1
grandchild.tabIndex = -1
parent.classList.add('parent')
child.classList.add('child')
grandchild.classList.add('grandchild')
child.appendChild(grandchild)
parent.appendChild(child)
document.body.appendChild(parent)
afterEach ->
document.body.blur()
document.body.removeChild(parent)
describe "::add(itemsBySelector)", ->
it "can add top-level menu items that can be removed with the returned disposable", ->
disposable = contextMenu.add
'.parent': [{label: 'A', command: 'a'}]
'.child': [{label: 'B', command: 'b'}]
'.grandchild': [{label: 'C', command: 'c'}]
expect(contextMenu.templateForElement(grandchild)).toEqual [
{label: 'C', command: 'c'}
{label: 'B', command: 'b'}
{label: 'A', command: 'a'}
]
disposable.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual []
it "can add submenu items to existing menus that can be removed with the returned disposable", ->
disposable1 = contextMenu.add
'.grandchild': [{label: 'A', submenu: [{label: 'B', command: 'b'}]}]
disposable2 = contextMenu.add
'.grandchild': [{label: 'A', submenu: [{label: 'C', command: 'c'}]}]
expect(contextMenu.templateForElement(grandchild)).toEqual [{
label: 'A',
submenu: [
{label: 'B', command: 'b'}
{label: 'C', command: 'c'}
]
}]
disposable2.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual [{
label: 'A',
submenu: [
{label: 'B', command: 'b'}
]
}]
disposable1.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual []
it "favors the most specific / recently added item in the case of a duplicate label", ->
grandchild.classList.add('foo')
disposable1 = contextMenu.add
'.grandchild': [{label: 'A', command: 'a'}]
disposable2 = contextMenu.add
'.grandchild.foo': [{label: 'A', command: 'b'}]
disposable3 = contextMenu.add
'.grandchild': [{label: 'A', command: 'c'}]
disposable4 = contextMenu.add
'.child': [{label: 'A', command: 'd'}]
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'b'}]
disposable2.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'c'}]
disposable3.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}]
disposable1.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'd'}]
it "allows multiple separators, but not adjacent to each other", ->
contextMenu.add
'.grandchild': [
{label: 'A', command: 'a'},
{type: 'separator'},
{type: 'separator'},
{label: 'B', command: 'b'},
{type: 'separator'},
{type: 'separator'},
{label: 'C', command: 'c'}
]
expect(contextMenu.templateForElement(grandchild)).toEqual [
{label: 'A', command: 'a'},
{type: 'separator'},
{label: 'B', command: 'b'},
{type: 'separator'},
{label: 'C', command: 'c'}
]
it "excludes items marked for display in devMode unless in dev mode", ->
disposable1 = contextMenu.add
'.grandchild': [{label: 'A', command: 'a', devMode: true}, {label: 'B', command: 'b', devMode: false}]
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'B', command: 'b'}]
contextMenu.devMode = true
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}, {label: 'B', command: 'b'}]
it "allows items to be associated with `created` hooks which are invoked on template construction with the item and event", ->
createdEvent = null
item = {
label: 'A',
command: 'a',
created: (event) ->
@command = 'b'
createdEvent = event
}
contextMenu.add('.grandchild': [item])
dispatchedEvent = {target: grandchild}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual [{label: 'A', command: 'b'}]
expect(item.command).toBe 'a' # doesn't modify original item template
expect(createdEvent).toBe dispatchedEvent
it "allows items to be associated with `shouldDisplay` hooks which are invoked on construction to determine whether the item should be included", ->
shouldDisplayEvent = null
shouldDisplay = true
item = {
label: 'A',
command: 'a',
shouldDisplay: (event) ->
@foo = 'bar'
shouldDisplayEvent = event
shouldDisplay
}
contextMenu.add('.grandchild': [item])
dispatchedEvent = {target: grandchild}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual [{label: 'A', command: 'a'}]
expect(item.foo).toBeUndefined() # doesn't modify original item template
expect(shouldDisplayEvent).toBe dispatchedEvent
shouldDisplay = false
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual []
it "prunes a trailing separator", ->
contextMenu.add
'.grandchild': [
{label: 'A', command: 'a'},
{type: 'separator'},
{label: 'B', command: 'b'},
{type: 'separator'}
]
expect(contextMenu.templateForEvent({target: grandchild}).length).toBe(3)
it "prunes a leading separator", ->
contextMenu.add
'.grandchild': [
{type: 'separator'},
{label: 'A', command: 'a'},
{type: 'separator'},
{label: 'B', command: 'b'}
]
expect(contextMenu.templateForEvent({target: grandchild}).length).toBe(3)
it "prunes duplicate separators", ->
contextMenu.add
'.grandchild': [
{label: 'A', command: 'a'},
{type: 'separator'},
{type: 'separator'},
{label: 'B', command: 'b'}
]
expect(contextMenu.templateForEvent({target: grandchild}).length).toBe(3)
it "prunes all redundant separators", ->
contextMenu.add
'.grandchild': [
{type: 'separator'},
{type: 'separator'},
{label: 'A', command: 'a'},
{type: 'separator'},
{type: 'separator'},
{label: 'B', command: 'b'}
{label: 'C', command: 'c'}
{type: 'separator'},
{type: 'separator'},
]
expect(contextMenu.templateForEvent({target: grandchild}).length).toBe(4)
it "throws an error when the selector is invalid", ->
addError = null
try
contextMenu.add '<>': [{label: 'A', command: 'a'}]
catch error
addError = error
expect(addError.message).toContain('<>')
it "calls `created` hooks for submenu items", ->
item = {
label: 'A',
command: 'B',
submenu: [
{
label: 'C',
created: (event) -> @label = 'D',
}
]
}
contextMenu.add('.grandchild': [item])
dispatchedEvent = {target: grandchild}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual(
[
label: 'A',
command: 'B',
submenu: [
{
label: 'D',
}
]
])
describe "::templateForEvent(target)", ->
[keymaps, item] = []
beforeEach ->
keymaps = atom.keymaps.add('source', {
'.child': {
'ctrl-a': 'test:my-command',
'shift-b': 'test:my-other-command'
}
})
item = {
label: 'My Command',
command: 'test:my-command',
submenu: [
{
label: 'My Other Command',
command: 'test:my-other-command',
}
]
}
contextMenu.add('.parent': [item])
afterEach ->
keymaps.dispose()
it "adds Electron-style accelerators to items that have keybindings", ->
child.focus()
dispatchedEvent = {target: child}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual(
[
label: 'My Command',
command: 'test:my-command',
accelerator: 'Ctrl+A',
submenu: [
{
label: 'My Other Command',
command: 'test:my-other-command',
accelerator: 'Shift+B',
}
]
])
it "adds accelerators when a parent node has key bindings for a given command", ->
grandchild.focus()
dispatchedEvent = {target: grandchild}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual(
[
label: 'My Command',
command: 'test:my-command',
accelerator: 'Ctrl+A',
submenu: [
{
label: 'My Other Command',
command: 'test:my-other-command',
accelerator: 'Shift+B',
}
]
])
it "does not add accelerators when a child node has key bindings for a given command", ->
parent.focus()
dispatchedEvent = {target: parent}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual(
[
label: 'My Command',
command: 'test:my-command',
submenu: [
{
label: 'My Other Command',
command: 'test:my-other-command',
}
]
])
it "adds accelerators based on focus, not context menu target", ->
grandchild.focus()
dispatchedEvent = {target: parent}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual(
[
label: 'My Command',
command: 'test:my-command',
accelerator: 'Ctrl+A',
submenu: [
{
label: 'My Other Command',
command: 'test:my-other-command',
accelerator: 'Shift+B',
}
]
])