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',
            }
          ]
        ])

    it "does not add accelerators for multi-keystroke key bindings", ->
      atom.keymaps.add('source', {
        '.child': {
          'ctrl-a ctrl-b': 'test:multi-keystroke-command'
        }
      })
      contextMenu.clear()
      contextMenu.add('.parent': [{
        label: 'Multi-keystroke command',
        command: 'test:multi-keystroke-command',
      }])

      child.focus()

      label =
        if process.platform is 'darwin'
          '⌃A ⌃B'
        else
          'Ctrl+A Ctrl+B'
      expect(contextMenu.templateForEvent({target: child})).toEqual([{
        label: "Multi-keystroke command [#{label}]",
        command: 'test:multi-keystroke-command',
      }])

  describe "::templateForEvent(target) (sorting)", ->
    it "applies simple sorting rules", ->
      contextMenu.add('.parent': [{
        label: 'My Command',
        command: "test:my-command",
        after: ["test:my-other-command"]
      }, {
        label: 'My Other Command',
        command: "test:my-other-command",
      }])
      dispatchedEvent = {target: parent}
      expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([{
        label: 'My Other Command',
        command: 'test:my-other-command',
      }, {
        label: 'My Command',
        command: 'test:my-command',
        after: ["test:my-other-command"]
      }])

    it "applies sorting rules recursively to submenus", ->
      contextMenu.add('.parent': [{
        submenu: [{
          label: 'My Command',
          command: "test:my-command",
          after: ["test:my-other-command"]
        }, {
          label: 'My Other Command',
          command: "test:my-other-command",
        }]
      }])
      dispatchedEvent = {target: parent}
      expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([{
        submenu: [{
          label: 'My Other Command',
          command: 'test:my-other-command',
        }, {
          label: 'My Command',
          command: 'test:my-command',
          after: ["test:my-other-command"]
        }]
      }])
