From 0c9fd4603099a2424966e0f888d1286305bfa47a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 17 Sep 2014 19:03:52 -0600 Subject: [PATCH 01/14] Add CommandRegistry::dispatch for tests --- spec/command-registry-spec.coffee | 14 ++++++ src/command-registry.coffee | 79 ++++++++++++++++++------------- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index 8985e075b..1bb065359 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -124,3 +124,17 @@ describe "CommandRegistry", -> {name: 'namespace:command-2', displayName: 'Namespace: Command 2'} {name: 'namespace:command-1', displayName: 'Namespace: Command 1'} ] + + describe "::dispatch(target, commandName)", -> + it "simulates invocation of the given command ", -> + called = false + registry.add '.grandchild', 'command', (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 diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 23a54fea2..984a604f2 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -51,8 +51,8 @@ class CommandRegistry @rootNode = newRootNode for commandName of @listenersByCommandName - oldRootNode?.removeEventListener(commandName, @dispatchCommand, true) - newRootNode?.addEventListener(commandName, @dispatchCommand, true) + oldRootNode?.removeEventListener(commandName, @handleCommandEvent, true) + newRootNode?.addEventListener(commandName, @handleCommandEvent, true) # Public: Add one or more command listeners associated with a selector. # @@ -88,7 +88,7 @@ class CommandRegistry return disposable unless @listenersByCommandName[commandName]? - @rootNode?.addEventListener(commandName, @dispatchCommand, true) + @rootNode?.addEventListener(commandName, @handleCommandEvent, true) @listenersByCommandName[commandName] = [] listener = new CommandListener(selector, callback) @@ -99,35 +99,7 @@ class CommandRegistry listenersForCommand.splice(listenersForCommand.indexOf(listener), 1) if listenersForCommand.length is 0 delete @listenersByCommandName[commandName] - @rootNode.removeEventListener(commandName, @dispatchCommand, true) - - dispatchCommand: (event) => - propagationStopped = false - immediatePropagationStopped = false - currentTarget = event.target - - syntheticEvent = Object.create event, - eventPhase: value: Event.BUBBLING_PHASE - currentTarget: get: -> currentTarget - stopPropagation: value: -> - propagationStopped = true - stopImmediatePropagation: value: -> - propagationStopped = true - immediatePropagationStopped = true - - loop - matchingListeners = - @listenersByCommandName[event.type] - .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) - .sort (a, b) -> a.compare(b) - - for listener in matchingListeners - break if immediatePropagationStopped - listener.callback.call(currentTarget, syntheticEvent) - - break if propagationStopped - break if currentTarget is @rootNode - currentTarget = currentTarget.parentNode + @rootNode.removeEventListener(commandName, @handleCommandEvent, true) # Public: Find all registered commands matching a query. # @@ -163,6 +135,49 @@ class CommandRegistry commands + # Public: Simulate the dispatch of a command on a DOM node. + # + # This can be useful for testing when you want to simulate the invocation of a + # command on a detached DOM node. Otherwise, the DOM node in question needs to + # be attached to the document so the event bubbles up to the root node to be + # processed. + # + # * `target` The DOM node at which to start bubbling the command event. + # * `commandName` {String} indicating the name of the command to dispatch. + dispatch: (target, commandName) -> + event = new CustomEvent(commandName, bubbles: true) + eventWithTarget = Object.create(event, target: value: target) + @handleCommandEvent(eventWithTarget) + + handleCommandEvent: (event) => + propagationStopped = false + immediatePropagationStopped = false + currentTarget = event.target + + syntheticEvent = Object.create event, + eventPhase: value: Event.BUBBLING_PHASE + currentTarget: get: -> currentTarget + stopPropagation: value: -> + propagationStopped = true + stopImmediatePropagation: value: -> + propagationStopped = true + immediatePropagationStopped = true + + loop + matchingListeners = + @listenersByCommandName[event.type] + .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) + .sort (a, b) -> a.compare(b) + + for listener in matchingListeners + break if immediatePropagationStopped + listener.callback.call(currentTarget, syntheticEvent) + + break unless currentTarget? + break if currentTarget is @rootNode + break if propagationStopped + currentTarget = currentTarget.parentNode + clear: -> @listenersByCommandName = {} From 67ff8f43828f2b0644cf9f0b5addbe83b4c7e94e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 17 Sep 2014 19:11:16 -0600 Subject: [PATCH 02/14] =?UTF-8?q?Don=E2=80=99t=20clear=20commands=20after?= =?UTF-8?q?=20specs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commands are typically registered once at eval time. Clearing them means that commands aren’t available except in the first spec. --- spec/spec-helper.coffee | 2 -- src/command-registry.coffee | 3 --- 2 files changed, 5 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 48fad5c8c..c4f39bbb7 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -121,8 +121,6 @@ beforeEach -> addCustomMatchers(this) afterEach -> - atom.commands.clear() - atom.packages.deactivatePackages() atom.menu.template = [] diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 984a604f2..dc173dc97 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -178,9 +178,6 @@ class CommandRegistry break if propagationStopped currentTarget = currentTarget.parentNode - clear: -> - @listenersByCommandName = {} - class CommandListener constructor: (@selector, @callback) -> @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) From 09b5ac887abda0ef561725bd89bdb9bb80ab280e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 18 Sep 2014 12:40:55 -0600 Subject: [PATCH 03/14] Return whether a dispatched command matched a listener --- spec/command-registry-spec.coffee | 7 +++++++ src/command-registry.coffee | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index 1bb065359..98c480375 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -138,3 +138,10 @@ describe "CommandRegistry", -> registry.dispatch(grandchild, 'command') expect(called).toBe true + + it "returns a boolean indicating whether any listeners matched the command", -> + registry.add '.grandchild', 'command', -> + + expect(registry.dispatch(grandchild, 'command')).toBe true + expect(registry.dispatch(grandchild, 'bogus')).toBe false + expect(registry.dispatch(parent, 'command')).toBe false diff --git a/src/command-registry.coffee b/src/command-registry.coffee index dc173dc97..11dec2dca 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -152,6 +152,7 @@ class CommandRegistry handleCommandEvent: (event) => propagationStopped = false immediatePropagationStopped = false + matched = false currentTarget = event.target syntheticEvent = Object.create event, @@ -169,6 +170,8 @@ class CommandRegistry .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) .sort (a, b) -> a.compare(b) + matched = true if matchingListeners.length > 0 + for listener in matchingListeners break if immediatePropagationStopped listener.callback.call(currentTarget, syntheticEvent) @@ -178,6 +181,8 @@ class CommandRegistry break if propagationStopped currentTarget = currentTarget.parentNode + matched + class CommandListener constructor: (@selector, @callback) -> @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) From c094b7a0efc190d4601b43f46d64c909c44a655d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 13:25:37 -0600 Subject: [PATCH 04/14] Extract package-manager-specs from atom-specs --- spec/atom-spec.coffee | 535 ------------------------------ spec/package-manager-spec.coffee | 541 +++++++++++++++++++++++++++++++ 2 files changed, 541 insertions(+), 535 deletions(-) create mode 100644 spec/package-manager-spec.coffee diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 5389886ca..9fcfa1f2b 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -25,541 +25,6 @@ describe "the `atom` global", -> atom.setSize(100, 400) expect(atom.getSize()).toEqual width: 100, height: 400 - describe "package lifecycle methods", -> - describe ".loadPackage(name)", -> - it "continues if the package has an invalid package.json", -> - spyOn(console, 'warn') - atom.config.set("core.disabledPackages", []) - expect(-> atom.packages.loadPackage("package-with-broken-package-json")).not.toThrow() - - it "continues if the package has an invalid keymap", -> - atom.config.set("core.disabledPackages", []) - expect(-> atom.packages.loadPackage("package-with-broken-keymap")).not.toThrow() - - describe ".unloadPackage(name)", -> - describe "when the package is active", -> - it "throws an error", -> - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-main').then (p) -> pack = p - - runs -> - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - expect( -> atom.packages.unloadPackage(pack.name)).toThrow() - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - - describe "when the package is not loaded", -> - it "throws an error", -> - expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() - expect( -> atom.packages.unloadPackage('unloaded')).toThrow() - expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() - - describe "when the package is loaded", -> - it "no longers reports it as being loaded", -> - pack = atom.packages.loadPackage('package-with-main') - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - atom.packages.unloadPackage(pack.name) - expect(atom.packages.isPackageLoaded(pack.name)).toBeFalsy() - - describe ".activatePackage(id)", -> - describe "atom packages", -> - describe "when called multiple times", -> - it "it only calls activate on the package once", -> - spyOn(Package.prototype, 'activateNow').andCallThrough() - atom.packages.activatePackage('package-with-index') - atom.packages.activatePackage('package-with-index') - - waitsForPromise -> - atom.packages.activatePackage('package-with-index') - - runs -> - expect(Package.prototype.activateNow.callCount).toBe 1 - - describe "when the package has a main module", -> - describe "when the metadata specifies a main module path˜", -> - it "requires the module at the specified path", -> - mainModule = require('./fixtures/packages/package-with-main/main-module') - spyOn(mainModule, 'activate') - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-main').then (p) -> pack = p - - runs -> - expect(mainModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe mainModule - - describe "when the metadata does not specify a main module", -> - it "requires index.coffee", -> - indexModule = require('./fixtures/packages/package-with-index/index') - spyOn(indexModule, 'activate') - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-index').then (p) -> pack = p - - runs -> - expect(indexModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe indexModule - - it "assigns config defaults from the module", -> - expect(atom.config.get('package-with-config-defaults.numbers.one')).toBeUndefined() - - waitsForPromise -> - atom.packages.activatePackage('package-with-config-defaults') - - runs -> - expect(atom.config.get('package-with-config-defaults.numbers.one')).toBe 1 - expect(atom.config.get('package-with-config-defaults.numbers.two')).toBe 2 - - describe "when the package metadata includes activation events", -> - [mainModule, promise] = [] - - beforeEach -> - mainModule = require './fixtures/packages/package-with-activation-events/index' - spyOn(mainModule, 'activate').andCallThrough() - spyOn(Package.prototype, 'requireMainModule').andCallThrough() - - promise = atom.packages.activatePackage('package-with-activation-events') - - it "defers requiring/activating the main module until an activation event bubbles to the root view", -> - expect(promise.isFulfilled()).not.toBeTruthy() - atom.workspaceView.trigger 'activation-event' - - waitsForPromise -> - promise - - it "triggers the activation event on all handlers registered during activation", -> - waitsForPromise -> - atom.workspaceView.open() - - runs -> - editorView = atom.workspaceView.getActiveView() - eventHandler = jasmine.createSpy("activation-event") - editorView.command 'activation-event', eventHandler - editorView.trigger 'activation-event' - expect(mainModule.activate.callCount).toBe 1 - expect(mainModule.activationEventCallCount).toBe 1 - expect(eventHandler.callCount).toBe 1 - editorView.trigger 'activation-event' - expect(mainModule.activationEventCallCount).toBe 2 - expect(eventHandler.callCount).toBe 2 - expect(mainModule.activate.callCount).toBe 1 - - it "activates the package immediately when the events are empty", -> - mainModule = require './fixtures/packages/package-with-empty-activation-events/index' - spyOn(mainModule, 'activate').andCallThrough() - - waitsForPromise -> - atom.packages.activatePackage('package-with-empty-activation-events') - - runs -> - expect(mainModule.activate.callCount).toBe 1 - - describe "when the package has no main module", -> - it "does not throw an exception", -> - spyOn(console, "error") - spyOn(console, "warn").andCallThrough() - expect(-> atom.packages.activatePackage('package-without-module')).not.toThrow() - expect(console.error).not.toHaveBeenCalled() - expect(console.warn).not.toHaveBeenCalled() - - it "passes the activate method the package's previously serialized state if it exists", -> - pack = null - waitsForPromise -> - atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p - - runs -> - expect(pack.mainModule.someNumber).not.toBe 77 - pack.mainModule.someNumber = 77 - atom.packages.deactivatePackage("package-with-serialization") - spyOn(pack.mainModule, 'activate').andCallThrough() - atom.packages.activatePackage("package-with-serialization") - expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) - - it "logs warning instead of throwing an exception if the package fails to load", -> - atom.config.set("core.disabledPackages", []) - spyOn(console, "warn") - expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).not.toThrow() - expect(console.warn).toHaveBeenCalled() - - describe "keymap loading", -> - describe "when the metadata does not contain a 'keymaps' manifest", -> - it "loads all the .cson/.json files in the keymaps directory", -> - element1 = $$ -> @div class: 'test-1' - element2 = $$ -> @div class: 'test-2' - element3 = $$ -> @div class: 'test-3' - - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 - - atom.packages.activatePackage("package-with-keymaps") - - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe "test-1" - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])[0].command).toBe "test-2" - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 - - describe "when the metadata contains a 'keymaps' manifest", -> - it "loads only the keymaps specified by the manifest, in the specified order", -> - element1 = $$ -> @div class: 'test-1' - element3 = $$ -> @div class: 'test-3' - - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])).toHaveLength 0 - - atom.packages.activatePackage("package-with-keymaps-manifest") - - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe 'keymap-1' - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-n', target:element1[0])[0].command).toBe 'keymap-2' - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-y', target:element3[0])).toHaveLength 0 - - describe "menu loading", -> - beforeEach -> - atom.contextMenu.definitions = [] - atom.menu.template = [] - - describe "when the metadata does not contain a 'menus' manifest", -> - it "loads all the .cson/.json files in the menus directory", -> - element = ($$ -> @div class: 'test-1')[0] - - expect(atom.contextMenu.definitionsForElement(element)).toEqual [] - - atom.packages.activatePackage("package-with-menus") - - expect(atom.menu.template.length).toBe 2 - expect(atom.menu.template[0].label).toBe "Second to Last" - expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 1" - expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 2" - expect(atom.contextMenu.definitionsForElement(element)[2].label).toBe "Menu item 3" - - describe "when the metadata contains a 'menus' manifest", -> - it "loads only the menus specified by the manifest, in the specified order", -> - element = ($$ -> @div class: 'test-1')[0] - - expect(atom.contextMenu.definitionsForElement(element)).toEqual [] - - atom.packages.activatePackage("package-with-menus-manifest") - - expect(atom.menu.template[0].label).toBe "Second to Last" - expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 2" - expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 1" - expect(atom.contextMenu.definitionsForElement(element)[2]).toBeUndefined() - - describe "stylesheet loading", -> - describe "when the metadata contains a 'stylesheets' manifest", -> - it "loads stylesheets from the stylesheets directory as specified by the manifest", -> - one = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/1.css") - two = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/2.less") - three = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/3.css") - - one = atom.themes.stringToId(one) - two = atom.themes.stringToId(two) - three = atom.themes.stringToId(three) - - expect(atom.themes.stylesheetElementForId(one)).toBeNull() - expect(atom.themes.stylesheetElementForId(two)).toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - - atom.packages.activatePackage("package-with-stylesheets-manifest") - - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - expect($('#jasmine-content').css('font-size')).toBe '1px' - - describe "when the metadata does not contain a 'stylesheets' manifest", -> - it "loads all stylesheets from the stylesheets directory", -> - one = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/1.css") - two = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/2.less") - three = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/3.css") - - - one = atom.themes.stringToId(one) - two = atom.themes.stringToId(two) - three = atom.themes.stringToId(three) - - expect(atom.themes.stylesheetElementForId(one)).toBeNull() - expect(atom.themes.stylesheetElementForId(two)).toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - - atom.packages.activatePackage("package-with-stylesheets") - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() - expect($('#jasmine-content').css('font-size')).toBe '3px' - - describe "grammar loading", -> - it "loads the package's grammars", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-grammars') - - runs -> - expect(atom.syntax.selectGrammar('a.alot').name).toBe 'Alot' - expect(atom.syntax.selectGrammar('a.alittle').name).toBe 'Alittle' - - describe "scoped-property loading", -> - it "loads the scoped properties", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-scoped-properties") - - runs -> - expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a' - - describe "converted textmate packages", -> - it "loads the package's grammars", -> - expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" - - waitsForPromise -> - atom.packages.activatePackage('language-ruby') - - runs -> - expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby" - - it "loads the translated scoped properties", -> - expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined() - - waitsForPromise -> - atom.packages.activatePackage('language-ruby') - - runs -> - expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBe '# ' - - describe ".deactivatePackage(id)", -> - describe "atom packages", -> - it "calls `deactivate` on the package's main module if activate was successful", -> - pack = null - waitsForPromise -> - atom.packages.activatePackage("package-with-deactivate").then (p) -> pack = p - - runs -> - expect(atom.packages.isPackageActive("package-with-deactivate")).toBeTruthy() - spyOn(pack.mainModule, 'deactivate').andCallThrough() - - atom.packages.deactivatePackage("package-with-deactivate") - expect(pack.mainModule.deactivate).toHaveBeenCalled() - expect(atom.packages.isPackageActive("package-with-module")).toBeFalsy() - - spyOn(console, 'warn') - - badPack = null - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p - - runs -> - expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeTruthy() - spyOn(badPack.mainModule, 'deactivate').andCallThrough() - - atom.packages.deactivatePackage("package-that-throws-on-activate") - expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() - expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy() - - it "does not serialize packages that have not been activated called on their main module", -> - spyOn(console, 'warn') - badPack = null - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p - - runs -> - spyOn(badPack.mainModule, 'serialize').andCallThrough() - - atom.packages.deactivatePackage("package-that-throws-on-activate") - expect(badPack.mainModule.serialize).not.toHaveBeenCalled() - - it "absorbs exceptions that are thrown by the package module's serialize method", -> - spyOn(console, 'error') - - waitsForPromise -> - atom.packages.activatePackage('package-with-serialize-error') - - waitsForPromise -> - atom.packages.activatePackage('package-with-serialization') - - runs -> - atom.packages.deactivatePackages() - expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() - expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1 - expect(console.error).toHaveBeenCalled() - - it "absorbs exceptions that are thrown by the package module's deactivate method", -> - spyOn(console, 'error') - - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-deactivate") - - runs -> - expect(-> atom.packages.deactivatePackage("package-that-throws-on-deactivate")).not.toThrow() - expect(console.error).toHaveBeenCalled() - - it "removes the package's grammars", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-grammars') - - runs -> - atom.packages.deactivatePackage('package-with-grammars') - expect(atom.syntax.selectGrammar('a.alot').name).toBe 'Null Grammar' - expect(atom.syntax.selectGrammar('a.alittle').name).toBe 'Null Grammar' - - it "removes the package's keymaps", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-keymaps') - - runs -> - atom.packages.deactivatePackage('package-with-keymaps') - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target: ($$ -> @div class: 'test-1')[0])).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target: ($$ -> @div class: 'test-2')[0])).toHaveLength 0 - - it "removes the package's stylesheets", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-stylesheets') - - runs -> - atom.packages.deactivatePackage('package-with-stylesheets') - one = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/1.css") - two = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/2.less") - three = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/3.css") - expect(atom.themes.stylesheetElementForId(one)).not.toExist() - expect(atom.themes.stylesheetElementForId(two)).not.toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() - - it "removes the package's scoped-properties", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-scoped-properties") - - runs -> - expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a' - atom.packages.deactivatePackage("package-with-scoped-properties") - expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBeUndefined() - - describe "textmate packages", -> - it "removes the package's grammars", -> - expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" - - waitsForPromise -> - atom.packages.activatePackage('language-ruby') - - runs -> - expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby" - atom.packages.deactivatePackage('language-ruby') - expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" - - it "removes the package's scoped properties", -> - waitsForPromise -> - atom.packages.activatePackage('language-ruby') - - runs -> - atom.packages.deactivatePackage('language-ruby') - expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined() - - describe ".activate()", -> - packageActivator = null - themeActivator = null - - beforeEach -> - spyOn(console, 'warn') - atom.packages.loadPackages() - - loadedPackages = atom.packages.getLoadedPackages() - expect(loadedPackages.length).toBeGreaterThan 0 - - packageActivator = spyOn(atom.packages, 'activatePackages') - themeActivator = spyOn(atom.themes, 'activatePackages') - - afterEach -> - atom.packages.unloadPackages() - - Syntax = require '../src/syntax' - atom.syntax = window.syntax = new Syntax() - - it "activates all the packages, and none of the themes", -> - atom.packages.activate() - - expect(packageActivator).toHaveBeenCalled() - expect(themeActivator).toHaveBeenCalled() - - packages = packageActivator.mostRecentCall.args[0] - expect(['atom', 'textmate']).toContain(pack.getType()) for pack in packages - - themes = themeActivator.mostRecentCall.args[0] - expect(['theme']).toContain(theme.getType()) for theme in themes - - describe ".enablePackage() and disablePackage()", -> - describe "with packages", -> - it ".enablePackage() enables a disabled package", -> - packageName = 'package-with-main' - atom.config.pushAtKeyPath('core.disabledPackages', packageName) - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).toContain packageName - - pack = atom.packages.enablePackage(packageName) - loadedPackages = atom.packages.getLoadedPackages() - activatedPackages = null - waitsFor -> - activatedPackages = atom.packages.getActivePackages() - activatedPackages.length > 0 - - runs -> - expect(loadedPackages).toContain(pack) - expect(activatedPackages).toContain(pack) - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - it ".disablePackage() disables an enabled package", -> - packageName = 'package-with-main' - waitsForPromise -> - atom.packages.activatePackage(packageName) - - runs -> - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - pack = atom.packages.disablePackage(packageName) - - activatedPackages = atom.packages.getActivePackages() - expect(activatedPackages).not.toContain(pack) - expect(atom.config.get('core.disabledPackages')).toContain packageName - - describe "with themes", -> - reloadedHandler = null - - beforeEach -> - waitsForPromise -> - atom.themes.activateThemes() - - afterEach -> - atom.themes.deactivateThemes() - - it ".enablePackage() and .disablePackage() enables and disables a theme", -> - packageName = 'theme-with-package-file' - - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - # enabling of theme - pack = atom.packages.enablePackage(packageName) - - waitsFor -> - pack in atom.packages.getActivePackages() - - runs -> - expect(atom.config.get('core.themes')).toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - reloadedHandler = jasmine.createSpy('reloadedHandler') - reloadedHandler.reset() - atom.themes.onDidReloadAll reloadedHandler - - pack = atom.packages.disablePackage(packageName) - - waitsFor -> - reloadedHandler.callCount is 1 - - runs -> - expect(atom.packages.getActivePackages()).not.toContain pack - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - describe ".isReleasedVersion()", -> it "returns false if the version is a SHA and true otherwise", -> version = '0.1.0' diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee new file mode 100644 index 000000000..925b3cec8 --- /dev/null +++ b/spec/package-manager-spec.coffee @@ -0,0 +1,541 @@ +{$, $$, WorkspaceView} = require 'atom' +Package = require '../src/package' + +describe "PackageManager", -> + beforeEach -> + atom.workspaceView = new WorkspaceView + + describe "::loadPackage(name)", -> + it "continues if the package has an invalid package.json", -> + spyOn(console, 'warn') + atom.config.set("core.disabledPackages", []) + expect(-> atom.packages.loadPackage("package-with-broken-package-json")).not.toThrow() + + it "continues if the package has an invalid keymap", -> + spyOn(console, 'warn') + atom.config.set("core.disabledPackages", []) + expect(-> atom.packages.loadPackage("package-with-broken-keymap")).not.toThrow() + + describe "::unloadPackage(name)", -> + describe "when the package is active", -> + it "throws an error", -> + pack = null + waitsForPromise -> + atom.packages.activatePackage('package-with-main').then (p) -> pack = p + + runs -> + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() + expect( -> atom.packages.unloadPackage(pack.name)).toThrow() + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() + + describe "when the package is not loaded", -> + it "throws an error", -> + expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() + expect( -> atom.packages.unloadPackage('unloaded')).toThrow() + expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() + + describe "when the package is loaded", -> + it "no longers reports it as being loaded", -> + pack = atom.packages.loadPackage('package-with-main') + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + atom.packages.unloadPackage(pack.name) + expect(atom.packages.isPackageLoaded(pack.name)).toBeFalsy() + + describe "::activatePackage(id)", -> + describe "atom packages", -> + describe "when called multiple times", -> + it "it only calls activate on the package once", -> + spyOn(Package.prototype, 'activateNow').andCallThrough() + atom.packages.activatePackage('package-with-index') + atom.packages.activatePackage('package-with-index') + + waitsForPromise -> + atom.packages.activatePackage('package-with-index') + + runs -> + expect(Package.prototype.activateNow.callCount).toBe 1 + + describe "when the package has a main module", -> + describe "when the metadata specifies a main module path˜", -> + it "requires the module at the specified path", -> + mainModule = require('./fixtures/packages/package-with-main/main-module') + spyOn(mainModule, 'activate') + pack = null + waitsForPromise -> + atom.packages.activatePackage('package-with-main').then (p) -> pack = p + + runs -> + expect(mainModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe mainModule + + describe "when the metadata does not specify a main module", -> + it "requires index.coffee", -> + indexModule = require('./fixtures/packages/package-with-index/index') + spyOn(indexModule, 'activate') + pack = null + waitsForPromise -> + atom.packages.activatePackage('package-with-index').then (p) -> pack = p + + runs -> + expect(indexModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe indexModule + + it "assigns config defaults from the module", -> + expect(atom.config.get('package-with-config-defaults.numbers.one')).toBeUndefined() + + waitsForPromise -> + atom.packages.activatePackage('package-with-config-defaults') + + runs -> + expect(atom.config.get('package-with-config-defaults.numbers.one')).toBe 1 + expect(atom.config.get('package-with-config-defaults.numbers.two')).toBe 2 + + describe "when the package metadata includes activation events", -> + [mainModule, promise] = [] + + beforeEach -> + mainModule = require './fixtures/packages/package-with-activation-events/index' + spyOn(mainModule, 'activate').andCallThrough() + spyOn(Package.prototype, 'requireMainModule').andCallThrough() + + promise = atom.packages.activatePackage('package-with-activation-events') + + it "defers requiring/activating the main module until an activation event bubbles to the root view", -> + expect(promise.isFulfilled()).not.toBeTruthy() + atom.workspaceView.trigger 'activation-event' + + waitsForPromise -> + promise + + it "triggers the activation event on all handlers registered during activation", -> + waitsForPromise -> + atom.workspaceView.open() + + runs -> + editorView = atom.workspaceView.getActiveView() + eventHandler = jasmine.createSpy("activation-event") + editorView.command 'activation-event', eventHandler + editorView.trigger 'activation-event' + expect(mainModule.activate.callCount).toBe 1 + expect(mainModule.activationEventCallCount).toBe 1 + expect(eventHandler.callCount).toBe 1 + editorView.trigger 'activation-event' + expect(mainModule.activationEventCallCount).toBe 2 + expect(eventHandler.callCount).toBe 2 + expect(mainModule.activate.callCount).toBe 1 + + it "activates the package immediately when the events are empty", -> + mainModule = require './fixtures/packages/package-with-empty-activation-events/index' + spyOn(mainModule, 'activate').andCallThrough() + + waitsForPromise -> + atom.packages.activatePackage('package-with-empty-activation-events') + + runs -> + expect(mainModule.activate.callCount).toBe 1 + + describe "when the package has no main module", -> + it "does not throw an exception", -> + spyOn(console, "error") + spyOn(console, "warn").andCallThrough() + expect(-> atom.packages.activatePackage('package-without-module')).not.toThrow() + expect(console.error).not.toHaveBeenCalled() + expect(console.warn).not.toHaveBeenCalled() + + it "passes the activate method the package's previously serialized state if it exists", -> + pack = null + waitsForPromise -> + atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p + + runs -> + expect(pack.mainModule.someNumber).not.toBe 77 + pack.mainModule.someNumber = 77 + atom.packages.deactivatePackage("package-with-serialization") + spyOn(pack.mainModule, 'activate').andCallThrough() + atom.packages.activatePackage("package-with-serialization") + expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) + + it "logs warning instead of throwing an exception if the package fails to load", -> + atom.config.set("core.disabledPackages", []) + spyOn(console, "warn") + expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).not.toThrow() + expect(console.warn).toHaveBeenCalled() + + describe "keymap loading", -> + describe "when the metadata does not contain a 'keymaps' manifest", -> + it "loads all the .cson/.json files in the keymaps directory", -> + element1 = $$ -> @div class: 'test-1' + element2 = $$ -> @div class: 'test-2' + element3 = $$ -> @div class: 'test-3' + + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 + + atom.packages.activatePackage("package-with-keymaps") + + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe "test-1" + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])[0].command).toBe "test-2" + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 + + describe "when the metadata contains a 'keymaps' manifest", -> + it "loads only the keymaps specified by the manifest, in the specified order", -> + element1 = $$ -> @div class: 'test-1' + element3 = $$ -> @div class: 'test-3' + + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])).toHaveLength 0 + + atom.packages.activatePackage("package-with-keymaps-manifest") + + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe 'keymap-1' + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-n', target:element1[0])[0].command).toBe 'keymap-2' + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-y', target:element3[0])).toHaveLength 0 + + describe "menu loading", -> + beforeEach -> + atom.contextMenu.definitions = [] + atom.menu.template = [] + + describe "when the metadata does not contain a 'menus' manifest", -> + it "loads all the .cson/.json files in the menus directory", -> + element = ($$ -> @div class: 'test-1')[0] + + expect(atom.contextMenu.definitionsForElement(element)).toEqual [] + + atom.packages.activatePackage("package-with-menus") + + expect(atom.menu.template.length).toBe 2 + expect(atom.menu.template[0].label).toBe "Second to Last" + expect(atom.menu.template[1].label).toBe "Last" + expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 1" + expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 2" + expect(atom.contextMenu.definitionsForElement(element)[2].label).toBe "Menu item 3" + + describe "when the metadata contains a 'menus' manifest", -> + it "loads only the menus specified by the manifest, in the specified order", -> + element = ($$ -> @div class: 'test-1')[0] + + expect(atom.contextMenu.definitionsForElement(element)).toEqual [] + + atom.packages.activatePackage("package-with-menus-manifest") + + expect(atom.menu.template[0].label).toBe "Second to Last" + expect(atom.menu.template[1].label).toBe "Last" + expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 2" + expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 1" + expect(atom.contextMenu.definitionsForElement(element)[2]).toBeUndefined() + + describe "stylesheet loading", -> + describe "when the metadata contains a 'stylesheets' manifest", -> + it "loads stylesheets from the stylesheets directory as specified by the manifest", -> + one = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/1.css") + two = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/2.less") + three = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/3.css") + + one = atom.themes.stringToId(one) + two = atom.themes.stringToId(two) + three = atom.themes.stringToId(three) + + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + + atom.packages.activatePackage("package-with-stylesheets-manifest") + + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + expect($('#jasmine-content').css('font-size')).toBe '1px' + + describe "when the metadata does not contain a 'stylesheets' manifest", -> + it "loads all stylesheets from the stylesheets directory", -> + one = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/1.css") + two = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/2.less") + three = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/3.css") + + + one = atom.themes.stringToId(one) + two = atom.themes.stringToId(two) + three = atom.themes.stringToId(three) + + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + + atom.packages.activatePackage("package-with-stylesheets") + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() + expect($('#jasmine-content').css('font-size')).toBe '3px' + + describe "grammar loading", -> + it "loads the package's grammars", -> + waitsForPromise -> + atom.packages.activatePackage('package-with-grammars') + + runs -> + expect(atom.syntax.selectGrammar('a.alot').name).toBe 'Alot' + expect(atom.syntax.selectGrammar('a.alittle').name).toBe 'Alittle' + + describe "scoped-property loading", -> + it "loads the scoped properties", -> + waitsForPromise -> + atom.packages.activatePackage("package-with-scoped-properties") + + runs -> + expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a' + + describe "converted textmate packages", -> + it "loads the package's grammars", -> + expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" + + waitsForPromise -> + atom.packages.activatePackage('language-ruby') + + runs -> + expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby" + + it "loads the translated scoped properties", -> + expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined() + + waitsForPromise -> + atom.packages.activatePackage('language-ruby') + + runs -> + expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBe '# ' + + describe "::deactivatePackage(id)", -> + describe "atom packages", -> + it "calls `deactivate` on the package's main module if activate was successful", -> + pack = null + waitsForPromise -> + atom.packages.activatePackage("package-with-deactivate").then (p) -> pack = p + + runs -> + expect(atom.packages.isPackageActive("package-with-deactivate")).toBeTruthy() + spyOn(pack.mainModule, 'deactivate').andCallThrough() + + atom.packages.deactivatePackage("package-with-deactivate") + expect(pack.mainModule.deactivate).toHaveBeenCalled() + expect(atom.packages.isPackageActive("package-with-module")).toBeFalsy() + + spyOn(console, 'warn') + + badPack = null + waitsForPromise -> + atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p + + runs -> + expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeTruthy() + spyOn(badPack.mainModule, 'deactivate').andCallThrough() + + atom.packages.deactivatePackage("package-that-throws-on-activate") + expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() + expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy() + + it "does not serialize packages that have not been activated called on their main module", -> + spyOn(console, 'warn') + badPack = null + waitsForPromise -> + atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p + + runs -> + spyOn(badPack.mainModule, 'serialize').andCallThrough() + + atom.packages.deactivatePackage("package-that-throws-on-activate") + expect(badPack.mainModule.serialize).not.toHaveBeenCalled() + + it "absorbs exceptions that are thrown by the package module's serialize method", -> + spyOn(console, 'error') + + waitsForPromise -> + atom.packages.activatePackage('package-with-serialize-error') + + waitsForPromise -> + atom.packages.activatePackage('package-with-serialization') + + runs -> + atom.packages.deactivatePackages() + expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() + expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1 + expect(console.error).toHaveBeenCalled() + + it "absorbs exceptions that are thrown by the package module's deactivate method", -> + spyOn(console, 'error') + + waitsForPromise -> + atom.packages.activatePackage("package-that-throws-on-deactivate") + + runs -> + expect(-> atom.packages.deactivatePackage("package-that-throws-on-deactivate")).not.toThrow() + expect(console.error).toHaveBeenCalled() + + it "removes the package's grammars", -> + waitsForPromise -> + atom.packages.activatePackage('package-with-grammars') + + runs -> + atom.packages.deactivatePackage('package-with-grammars') + expect(atom.syntax.selectGrammar('a.alot').name).toBe 'Null Grammar' + expect(atom.syntax.selectGrammar('a.alittle').name).toBe 'Null Grammar' + + it "removes the package's keymaps", -> + waitsForPromise -> + atom.packages.activatePackage('package-with-keymaps') + + runs -> + atom.packages.deactivatePackage('package-with-keymaps') + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target: ($$ -> @div class: 'test-1')[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target: ($$ -> @div class: 'test-2')[0])).toHaveLength 0 + + it "removes the package's stylesheets", -> + waitsForPromise -> + atom.packages.activatePackage('package-with-stylesheets') + + runs -> + atom.packages.deactivatePackage('package-with-stylesheets') + one = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/1.css") + two = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/2.less") + three = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/3.css") + expect(atom.themes.stylesheetElementForId(one)).not.toExist() + expect(atom.themes.stylesheetElementForId(two)).not.toExist() + expect(atom.themes.stylesheetElementForId(three)).not.toExist() + + it "removes the package's scoped-properties", -> + waitsForPromise -> + atom.packages.activatePackage("package-with-scoped-properties") + + runs -> + expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a' + atom.packages.deactivatePackage("package-with-scoped-properties") + expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBeUndefined() + + describe "textmate packages", -> + it "removes the package's grammars", -> + expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" + + waitsForPromise -> + atom.packages.activatePackage('language-ruby') + + runs -> + expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby" + atom.packages.deactivatePackage('language-ruby') + expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" + + it "removes the package's scoped properties", -> + waitsForPromise -> + atom.packages.activatePackage('language-ruby') + + runs -> + atom.packages.deactivatePackage('language-ruby') + expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined() + + describe "::activate()", -> + packageActivator = null + themeActivator = null + + beforeEach -> + spyOn(console, 'warn') + atom.packages.loadPackages() + + loadedPackages = atom.packages.getLoadedPackages() + expect(loadedPackages.length).toBeGreaterThan 0 + + packageActivator = spyOn(atom.packages, 'activatePackages') + themeActivator = spyOn(atom.themes, 'activatePackages') + + afterEach -> + atom.packages.unloadPackages() + + Syntax = require '../src/syntax' + atom.syntax = window.syntax = new Syntax() + + it "activates all the packages, and none of the themes", -> + atom.packages.activate() + + expect(packageActivator).toHaveBeenCalled() + expect(themeActivator).toHaveBeenCalled() + + packages = packageActivator.mostRecentCall.args[0] + expect(['atom', 'textmate']).toContain(pack.getType()) for pack in packages + + themes = themeActivator.mostRecentCall.args[0] + expect(['theme']).toContain(theme.getType()) for theme in themes + + describe "::enablePackage() and ::disablePackage()", -> + describe "with packages", -> + it ".enablePackage() enables a disabled package", -> + packageName = 'package-with-main' + atom.config.pushAtKeyPath('core.disabledPackages', packageName) + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).toContain packageName + + pack = atom.packages.enablePackage(packageName) + loadedPackages = atom.packages.getLoadedPackages() + activatedPackages = null + waitsFor -> + activatedPackages = atom.packages.getActivePackages() + activatedPackages.length > 0 + + runs -> + expect(loadedPackages).toContain(pack) + expect(activatedPackages).toContain(pack) + expect(atom.config.get('core.disabledPackages')).not.toContain packageName + + it ".disablePackage() disables an enabled package", -> + packageName = 'package-with-main' + waitsForPromise -> + atom.packages.activatePackage(packageName) + + runs -> + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).not.toContain packageName + + pack = atom.packages.disablePackage(packageName) + + activatedPackages = atom.packages.getActivePackages() + expect(activatedPackages).not.toContain(pack) + expect(atom.config.get('core.disabledPackages')).toContain packageName + + describe "with themes", -> + reloadedHandler = null + + beforeEach -> + waitsForPromise -> + atom.themes.activateThemes() + + afterEach -> + atom.themes.deactivateThemes() + + it ".enablePackage() and .disablePackage() enables and disables a theme", -> + packageName = 'theme-with-package-file' + + expect(atom.config.get('core.themes')).not.toContain packageName + expect(atom.config.get('core.disabledPackages')).not.toContain packageName + + # enabling of theme + pack = atom.packages.enablePackage(packageName) + + waitsFor -> + pack in atom.packages.getActivePackages() + + runs -> + expect(atom.config.get('core.themes')).toContain packageName + expect(atom.config.get('core.disabledPackages')).not.toContain packageName + + reloadedHandler = jasmine.createSpy('reloadedHandler') + reloadedHandler.reset() + atom.themes.onDidReloadAll reloadedHandler + + pack = atom.packages.disablePackage(packageName) + + waitsFor -> + reloadedHandler.callCount is 1 + + runs -> + expect(atom.packages.getActivePackages()).not.toContain pack + expect(atom.config.get('core.themes')).not.toContain packageName + expect(atom.config.get('core.themes')).not.toContain packageName + expect(atom.config.get('core.disabledPackages')).not.toContain packageName From a7196ec906b38baa4f0b31fef7699876eca64483 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 14:09:14 -0600 Subject: [PATCH 05/14] Dispatch activation commands with native DOM apis in specs --- spec/package-manager-spec.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 925b3cec8..29cca093f 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -96,6 +96,7 @@ describe "PackageManager", -> [mainModule, promise] = [] beforeEach -> + atom.workspaceView.attachToDom() mainModule = require './fixtures/packages/package-with-activation-events/index' spyOn(mainModule, 'activate').andCallThrough() spyOn(Package.prototype, 'requireMainModule').andCallThrough() @@ -104,7 +105,7 @@ describe "PackageManager", -> it "defers requiring/activating the main module until an activation event bubbles to the root view", -> expect(promise.isFulfilled()).not.toBeTruthy() - atom.workspaceView.trigger 'activation-event' + atom.workspaceView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) waitsForPromise -> promise @@ -117,11 +118,11 @@ describe "PackageManager", -> editorView = atom.workspaceView.getActiveView() eventHandler = jasmine.createSpy("activation-event") editorView.command 'activation-event', eventHandler - editorView.trigger 'activation-event' + editorView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) expect(mainModule.activate.callCount).toBe 1 expect(mainModule.activationEventCallCount).toBe 1 expect(eventHandler.callCount).toBe 1 - editorView.trigger 'activation-event' + editorView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) expect(mainModule.activationEventCallCount).toBe 2 expect(eventHandler.callCount).toBe 2 expect(mainModule.activate.callCount).toBe 1 From 2df5957f9b462fa73d3b950e319683cb685afecd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 15:41:44 -0600 Subject: [PATCH 06/14] Restore commands after each spec This commit adds the ability to get and restore snapshots of command listeners. Whatever commands are installed before specs begin are preserved, but commands added during specs are always cleared away. --- spec/command-registry-spec.coffee | 28 ++++++++++++++++++++++++++++ spec/spec-helper.coffee | 2 ++ src/command-registry.coffee | 11 +++++++++++ 3 files changed, 41 insertions(+) diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index 98c480375..03c0b4717 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -145,3 +145,31 @@ describe "CommandRegistry", -> expect(registry.dispatch(grandchild, 'command')).toBe true expect(registry.dispatch(grandchild, 'bogus')).toBe false expect(registry.dispatch(parent, 'command')).toBe false + + 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', -> + snapshot = registry.getSnapshot() + registry.add '.grandchild', 'namespace:command-3', -> + + expect(registry.findCommands(target: grandchild)[0..2]).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)[0..1]).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)[0..1]).toEqual [ + {name: 'namespace:command-2', displayName: 'Namespace: Command 2'} + {name: 'namespace:command-1', displayName: 'Namespace: Command 1'} + ] diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c4f39bbb7..a5a31ac4b 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -27,6 +27,7 @@ fixturePackagesPath = path.resolve(__dirname, './fixtures/packages') atom.packages.packageDirPaths.unshift(fixturePackagesPath) atom.keymaps.loadBundledKeymaps() keyBindingsToRestore = atom.keymaps.getKeyBindings() +commandsToRestore = atom.commands.getSnapshot() $(window).on 'core:close', -> window.close() $(window).on 'beforeunload', -> @@ -65,6 +66,7 @@ beforeEach -> atom.workspace = new Workspace() atom.keymaps.keyBindings = _.clone(keyBindingsToRestore) atom.commands.setRootNode(document.body) + atom.commands.restoreSnapshot(commandsToRestore) window.resetTimeouts() atom.packages.packageStates = {} diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 11dec2dca..fb05341f8 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -46,6 +46,8 @@ class CommandRegistry constructor: (@rootNode) -> @listenersByCommandName = {} + getRootNode: -> @rootNode + setRootNode: (newRootNode) -> oldRootNode = @rootNode @rootNode = newRootNode @@ -149,6 +151,15 @@ class CommandRegistry eventWithTarget = Object.create(event, target: value: target) @handleCommandEvent(eventWithTarget) + getSnapshot: -> + _.deepClone(@listenersByCommandName) + + restoreSnapshot: (snapshot) -> + rootNode = @getRootNode() + @setRootNode(null) # clear listeners for current commands + @listenersByCommandName = _.deepClone(snapshot) + @setRootNode(rootNode) # restore listeners for commands in snapshot + handleCommandEvent: (event) => propagationStopped = false immediatePropagationStopped = false From a492596f7ff217301551d9799a92472571441610 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 15:55:59 -0600 Subject: [PATCH 07/14] Allow atom.commands to participate in activationEvents * Activation events can be handled via atom.commands * Pre-existing listeners registered via atom.commands are disabled when replaying events for the activated package. --- .../index.coffee | 3 +++ spec/package-manager-spec.coffee | 10 +++++++- src/command-registry.coffee | 23 ++++++++++++++++++- src/package.coffee | 7 +++--- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/spec/fixtures/packages/package-with-activation-events/index.coffee b/spec/fixtures/packages/package-with-activation-events/index.coffee index fd91e6f79..3d7204dcb 100644 --- a/spec/fixtures/packages/package-with-activation-events/index.coffee +++ b/spec/fixtures/packages/package-with-activation-events/index.coffee @@ -6,8 +6,11 @@ class Foo module.exports = activateCallCount: 0 activationEventCallCount: 0 + activationCommandCallCount: 0 activate: -> @activateCallCount++ atom.workspaceView.getActiveView()?.command 'activation-event', => @activationEventCallCount++ + atom.commands.add '.workspace', 'activation-event', => + @activationCommandCallCount++ diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 29cca093f..bc9db25eb 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -98,6 +98,8 @@ describe "PackageManager", -> beforeEach -> atom.workspaceView.attachToDom() mainModule = require './fixtures/packages/package-with-activation-events/index' + mainModule.activationEventCallCount = 0 + mainModule.activationCommandCallCount = 0 spyOn(mainModule, 'activate').andCallThrough() spyOn(Package.prototype, 'requireMainModule').andCallThrough() @@ -116,15 +118,21 @@ describe "PackageManager", -> runs -> editorView = atom.workspaceView.getActiveView() - eventHandler = jasmine.createSpy("activation-event") + eventHandler = jasmine.createSpy("activation event handler") + globalCommandHandler = jasmine.createSpy("activation global command handler") editorView.command 'activation-event', eventHandler + commandDisposable = atom.commands.add '.workspace', 'activation-event', globalCommandHandler editorView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) expect(mainModule.activate.callCount).toBe 1 expect(mainModule.activationEventCallCount).toBe 1 + expect(mainModule.activationCommandCallCount).toBe 1 expect(eventHandler.callCount).toBe 1 + expect(globalCommandHandler.callCount).toBe 1 editorView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) expect(mainModule.activationEventCallCount).toBe 2 + expect(mainModule.activationCommandCallCount).toBe 2 expect(eventHandler.callCount).toBe 2 + expect(globalCommandHandler.callCount).toBe 2 expect(mainModule.activate.callCount).toBe 1 it "activates the package immediately when the events are empty", -> diff --git a/src/command-registry.coffee b/src/command-registry.coffee index fb05341f8..b3bb532db 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -137,6 +137,25 @@ class CommandRegistry commands + disableCommands: (target, commandName) -> + if @rootNode.contains(target) + currentTarget = target + else + currentTarget = @rootNode + + disabledListeners = [] + loop + for listener in @listenersByCommandName[commandName] ? [] + if currentTarget.webkitMatchesSelector(listener.selector) + listener.enabled = false + disabledListeners.push(listener) + + break if currentTarget is @rootNode + currentTarget = currentTarget.parentNode + + new Disposable => + listener.enabled = true for listener in disabledListeners + # Public: Simulate the dispatch of a command on a DOM node. # # This can be useful for testing when you want to simulate the invocation of a @@ -183,7 +202,7 @@ class CommandRegistry matched = true if matchingListeners.length > 0 - for listener in matchingListeners + for listener in matchingListeners when listener.enabled break if immediatePropagationStopped listener.callback.call(currentTarget, syntheticEvent) @@ -195,6 +214,8 @@ class CommandRegistry matched class CommandListener + enabled: true + constructor: (@selector, @callback) -> @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) @sequenceNumber = SequenceCount++ diff --git a/src/package.coffee b/src/package.coffee index c005ae664..df73bfc5f 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -342,12 +342,13 @@ class Package handleActivationEvent: (event) => bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event) + disabledCommands = atom.commands.disableCommands(event.target, event.type) @activateNow() - $ ?= require('./space-pen-extensions').$ - $(event.target).trigger(event) + event.target.dispatchEvent(new CustomEvent(event.type, bubbles: true)) @restoreEventHandlersOnBubblePath(bubblePathEventHandlers) + disabledCommands.dispose() @unsubscribeFromActivationEvents() - false + event.stopImmediatePropagation() unsubscribeFromActivationEvents: -> return unless atom.workspaceView? From 40f8b990d0af9fdf3a607b2fcc8823a1b6b871fe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 16:38:25 -0600 Subject: [PATCH 08/14] Handle dispatching non-existent commands --- src/command-registry.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command-registry.coffee b/src/command-registry.coffee index b3bb532db..945cbd450 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -196,7 +196,7 @@ class CommandRegistry loop matchingListeners = - @listenersByCommandName[event.type] + (@listenersByCommandName[event.type] ? []) .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) .sort (a, b) -> a.compare(b) From 066f6bf03cc410c151bd1b4d5eaf39519e9e94c1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 17:23:46 -0600 Subject: [PATCH 09/14] Forward stop[Immediate]Propagation to original event in CommandRegistry Previously, stopping propagation would work on the synthetic bubbling phase of the command registry itself, but the original event would continue to propagate which is counterintuitive. --- spec/command-registry-spec.coffee | 20 ++++++++++++++++++-- src/command-registry.coffee | 10 ++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index 03c0b4717..6d64bb9c0 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -66,8 +66,11 @@ describe "CommandRegistry", -> registry.add '.child', 'command', -> calls.push('child-2') registry.add '.child', 'command', (event) -> calls.push('child-1'); event.stopPropagation() - grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + 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", -> calls = [] @@ -76,8 +79,21 @@ describe "CommandRegistry", -> registry.add '.child', 'command', -> calls.push('child-2') registry.add '.child', 'command', (event) -> calls.push('child-1'); event.stopImmediatePropagation() - grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + 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", -> + calls = [] + + registry.add '.child', 'command', (event) -> event.preventDefault() + + dispatchedEvent = new CustomEvent('command', bubbles: true) + spyOn(dispatchedEvent, 'preventDefault') + grandchild.dispatchEvent(dispatchedEvent) + expect(dispatchedEvent.preventDefault).toHaveBeenCalled() it "allows listeners to be removed via a disposable returned by ::add", -> calls = [] diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 945cbd450..73cbdfe22 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -179,24 +179,26 @@ class CommandRegistry @listenersByCommandName = _.deepClone(snapshot) @setRootNode(rootNode) # restore listeners for commands in snapshot - handleCommandEvent: (event) => + handleCommandEvent: (originalEvent) => propagationStopped = false immediatePropagationStopped = false matched = false - currentTarget = event.target + currentTarget = originalEvent.target - syntheticEvent = Object.create event, + syntheticEvent = Object.create originalEvent, eventPhase: value: Event.BUBBLING_PHASE currentTarget: get: -> currentTarget stopPropagation: value: -> + originalEvent.stopPropagation() propagationStopped = true stopImmediatePropagation: value: -> + originalEvent.stopImmediatePropagation() propagationStopped = true immediatePropagationStopped = true loop matchingListeners = - (@listenersByCommandName[event.type] ? []) + (@listenersByCommandName[originalEvent.type] ? []) .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) .sort (a, b) -> a.compare(b) From 7d31b1727378b4ea615cea5d91535142a2fa70c0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 17:27:00 -0600 Subject: [PATCH 10/14] Use the CommandRegistry to register activation event listeners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commands registered with the command registry will always be handled first, so as long as we disable any listeners in the registry that were already invoked for the current command, we don’t need to disable jQuery methods before replaying the command after activating the package. This commit adds the ability to call .disableInvokedListeners on the event passed to the command listeners. This returns a function which can be called to reenable them. --- spec/package-manager-spec.coffee | 23 +++++++----- src/command-registry.coffee | 24 +++---------- src/package.coffee | 61 +++++++++++--------------------- 3 files changed, 40 insertions(+), 68 deletions(-) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index bc9db25eb..90bfb4849 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -93,7 +93,7 @@ describe "PackageManager", -> expect(atom.config.get('package-with-config-defaults.numbers.two')).toBe 2 describe "when the package metadata includes activation events", -> - [mainModule, promise] = [] + [mainModule, promise, workspaceCommandListener] = [] beforeEach -> atom.workspaceView.attachToDom() @@ -103,6 +103,9 @@ describe "PackageManager", -> spyOn(mainModule, 'activate').andCallThrough() spyOn(Package.prototype, 'requireMainModule').andCallThrough() + workspaceCommandListener = jasmine.createSpy('workspaceCommandListener') + atom.commands.add '.workspace', 'activation-event', workspaceCommandListener + promise = atom.packages.activatePackage('package-with-activation-events') it "defers requiring/activating the main module until an activation event bubbles to the root view", -> @@ -118,21 +121,23 @@ describe "PackageManager", -> runs -> editorView = atom.workspaceView.getActiveView() - eventHandler = jasmine.createSpy("activation event handler") - globalCommandHandler = jasmine.createSpy("activation global command handler") - editorView.command 'activation-event', eventHandler - commandDisposable = atom.commands.add '.workspace', 'activation-event', globalCommandHandler + legacyCommandListener = jasmine.createSpy("legacyCommandListener") + editorView.command 'activation-event', legacyCommandListener + editorCommandListener = jasmine.createSpy("editorCommandListener") + atom.commands.add '.editor', 'activation-event', editorCommandListener editorView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) expect(mainModule.activate.callCount).toBe 1 expect(mainModule.activationEventCallCount).toBe 1 expect(mainModule.activationCommandCallCount).toBe 1 - expect(eventHandler.callCount).toBe 1 - expect(globalCommandHandler.callCount).toBe 1 + expect(legacyCommandListener.callCount).toBe 1 + expect(editorCommandListener.callCount).toBe 1 + expect(workspaceCommandListener.callCount).toBe 1 editorView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) expect(mainModule.activationEventCallCount).toBe 2 expect(mainModule.activationCommandCallCount).toBe 2 - expect(eventHandler.callCount).toBe 2 - expect(globalCommandHandler.callCount).toBe 2 + expect(legacyCommandListener.callCount).toBe 2 + expect(editorCommandListener.callCount).toBe 2 + expect(workspaceCommandListener.callCount).toBe 2 expect(mainModule.activate.callCount).toBe 1 it "activates the package immediately when the events are empty", -> diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 73cbdfe22..800b48077 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -137,25 +137,6 @@ class CommandRegistry commands - disableCommands: (target, commandName) -> - if @rootNode.contains(target) - currentTarget = target - else - currentTarget = @rootNode - - disabledListeners = [] - loop - for listener in @listenersByCommandName[commandName] ? [] - if currentTarget.webkitMatchesSelector(listener.selector) - listener.enabled = false - disabledListeners.push(listener) - - break if currentTarget is @rootNode - currentTarget = currentTarget.parentNode - - new Disposable => - listener.enabled = true for listener in disabledListeners - # Public: Simulate the dispatch of a command on a DOM node. # # This can be useful for testing when you want to simulate the invocation of a @@ -184,6 +165,7 @@ class CommandRegistry immediatePropagationStopped = false matched = false currentTarget = originalEvent.target + invokedListeners = [] syntheticEvent = Object.create originalEvent, eventPhase: value: Event.BUBBLING_PHASE @@ -195,6 +177,9 @@ class CommandRegistry originalEvent.stopImmediatePropagation() propagationStopped = true immediatePropagationStopped = true + disableInvokedListeners: value: -> + listener.enabled = false for listener in invokedListeners + -> listener.enabled = true for listener in invokedListeners loop matchingListeners = @@ -206,6 +191,7 @@ class CommandRegistry for listener in matchingListeners when listener.enabled break if immediatePropagationStopped + invokedListeners.push(listener) listener.callback.call(currentTarget, syntheticEvent) break unless currentTarget? diff --git a/src/package.coffee b/src/package.coffee index df73bfc5f..953da9e22 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -5,7 +5,7 @@ async = require 'async' CSON = require 'season' fs = require 'fs-plus' EmitterMixin = require('emissary').Emitter -{Emitter} = require 'event-kit' +{Emitter, CompositeDisposable} = require 'event-kit' Q = require 'q' {deprecate} = require 'grim' @@ -270,7 +270,7 @@ class Package deactivate: -> @activationDeferred?.reject() @activationDeferred = null - @unsubscribeFromActivationEvents() + @activationCommandSubscriptions?.dispose() @deactivateResources() @deactivateConfig() if @mainActivated @@ -333,51 +333,32 @@ class Package subscribeToActivationEvents: -> return unless @metadata.activationEvents? + + @activationCommandSubscriptions = new CompositeDisposable + if _.isArray(@metadata.activationEvents) - atom.workspaceView.command(event, @handleActivationEvent) for event in @metadata.activationEvents + for eventName in @metadata.activationEvents + @activationCommandSubscriptions.add( + atom.commands.add('.workspace', eventName, @handleActivationEvent) + ) else if _.isString(@metadata.activationEvents) - atom.workspaceView.command(@metadata.activationEvents, @handleActivationEvent) + eventName = @metadata.activationEvents + @activationCommandSubscriptions.add( + atom.commands.add('.workspace', eventName, @handleActivationEvent) + ) else - atom.workspaceView.command(event, selector, @handleActivationEvent) for event, selector of @metadata.activationEvents + for eventName, selector of @metadata.activationEvents + @activationCommandSubscriptions.add( + atom.commands.add(selector, eventName, @handleActivationEvent) + ) handleActivationEvent: (event) => - bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event) - disabledCommands = atom.commands.disableCommands(event.target, event.type) + event.stopImmediatePropagation() + @activationCommandSubscriptions.dispose() + reenableInvokedListeners = event.disableInvokedListeners() @activateNow() event.target.dispatchEvent(new CustomEvent(event.type, bubbles: true)) - @restoreEventHandlersOnBubblePath(bubblePathEventHandlers) - disabledCommands.dispose() - @unsubscribeFromActivationEvents() - event.stopImmediatePropagation() - - unsubscribeFromActivationEvents: -> - return unless atom.workspaceView? - - if _.isArray(@metadata.activationEvents) - atom.workspaceView.off(event, @handleActivationEvent) for event in @metadata.activationEvents - else if _.isString(@metadata.activationEvents) - atom.workspaceView.off(@metadata.activationEvents, @handleActivationEvent) - else - atom.workspaceView.off(event, selector, @handleActivationEvent) for event, selector of @metadata.activationEvents - - disableEventHandlersOnBubblePath: (event) -> - bubblePathEventHandlers = [] - disabledHandler = -> - $ ?= require('./space-pen-extensions').$ - element = $(event.target) - while element.length - if eventHandlers = element.handlers()?[event.type] - for eventHandler in eventHandlers - eventHandler.disabledHandler = eventHandler.handler - eventHandler.handler = disabledHandler - bubblePathEventHandlers.push(eventHandler) - element = element.parent() - bubblePathEventHandlers - - restoreEventHandlersOnBubblePath: (eventHandlers) -> - for eventHandler in eventHandlers - eventHandler.handler = eventHandler.disabledHandler - delete eventHandler.disabledHandler + reenableInvokedListeners() # Does the given module path contain native code? isNativeModule: (modulePath) -> From c71457e9d47161fc001a69fd55011274ba296ce7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 17:32:48 -0600 Subject: [PATCH 11/14] Default selector to .workspace when subscribing to activation events --- src/package.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/package.coffee b/src/package.coffee index 953da9e22..6bae82e16 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -348,6 +348,7 @@ class Package ) else for eventName, selector of @metadata.activationEvents + selector ?= '.workspace' @activationCommandSubscriptions.add( atom.commands.add(selector, eventName, @handleActivationEvent) ) From 63181a17c8a82096cc1c5989f56ddc91205972a7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 18:09:28 -0600 Subject: [PATCH 12/14] Support activationCommands field in package.json This field mandates selectors in its structure and closely matches the API of `atom.commands.add`. It will supplant `activationEvents` moving forward. --- .../index.coffee | 2 + .../package.cson | 3 + spec/package-manager-spec.coffee | 17 ++++- src/package.coffee | 73 +++++++++++-------- 4 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 spec/fixtures/packages/package-with-activation-commands/index.coffee create mode 100644 spec/fixtures/packages/package-with-activation-commands/package.cson diff --git a/spec/fixtures/packages/package-with-activation-commands/index.coffee b/spec/fixtures/packages/package-with-activation-commands/index.coffee new file mode 100644 index 000000000..d17894702 --- /dev/null +++ b/spec/fixtures/packages/package-with-activation-commands/index.coffee @@ -0,0 +1,2 @@ +module.exports = + activate: -> diff --git a/spec/fixtures/packages/package-with-activation-commands/package.cson b/spec/fixtures/packages/package-with-activation-commands/package.cson new file mode 100644 index 000000000..ae14756ab --- /dev/null +++ b/spec/fixtures/packages/package-with-activation-commands/package.cson @@ -0,0 +1,3 @@ +activationCommands: + '.workspace': 'workspace-command' + '.editor': ['editor-command-1', 'editor-command-2'] diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 90bfb4849..d9a0e5828 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -92,7 +92,7 @@ describe "PackageManager", -> expect(atom.config.get('package-with-config-defaults.numbers.one')).toBe 1 expect(atom.config.get('package-with-config-defaults.numbers.two')).toBe 2 - describe "when the package metadata includes activation events", -> + describe "when the package metadata includes `activationEvents`", -> [mainModule, promise, workspaceCommandListener] = [] beforeEach -> @@ -150,6 +150,21 @@ describe "PackageManager", -> runs -> expect(mainModule.activate.callCount).toBe 1 + describe "when the package metadata includes `activationCommands`", -> + it "defers activation until one of the commands is invoked", -> + atom.workspaceView.attachToDom() + mainModule = require './fixtures/packages/package-with-activation-commands/index' + mainModule.commands = [] + spyOn(mainModule, 'activate').andCallThrough() + spyOn(Package.prototype, 'requireMainModule').andCallThrough() + + promise = atom.packages.activatePackage('package-with-activation-commands') + expect(promise.isFulfilled()).not.toBeTruthy() + + atom.workspaceView[0].dispatchEvent(new CustomEvent('workspace-command', bubbles: true)) + + waitsForPromise -> promise + describe "when the package has no main module", -> it "does not throw an exception", -> spyOn(console, "error") diff --git a/src/package.coffee b/src/package.coffee index 6bae82e16..30cd1ad2f 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -99,7 +99,7 @@ class Package @loadMenus() @loadStylesheets() @scopedPropertiesPromise = @loadScopedProperties() - @requireMainModule() unless @hasActivationEvents() + @requireMainModule() unless @hasActivationCommands() catch error console.warn "Failed to load package named '#{@name}'", error.stack ? error @@ -119,8 +119,8 @@ class Package @activationDeferred = Q.defer() @measure 'activateTime', => @activateResources() - if @hasActivationEvents() - @subscribeToActivationEvents() + if @hasActivationCommands() + @subscribeToActivationCommands() else @activateNow() @@ -319,41 +319,50 @@ class Package path.join(@path, 'index') @mainModulePath = fs.resolveExtension(mainModulePath, ["", _.keys(require.extensions)...]) - hasActivationEvents: -> - if _.isArray(@metadata.activationEvents) - return @metadata.activationEvents.some (activationEvent) -> - activationEvent?.length > 0 - else if _.isString(@metadata.activationEvents) - return @metadata.activationEvents.length > 0 - else if _.isObject(@metadata.activationEvents) - for event, selector of @metadata.activationEvents - return true if event.length > 0 and selector.length > 0 - + hasActivationCommands: -> + for selector, commands of @getActivationCommands() + return true if commands.length > 0 false - subscribeToActivationEvents: -> - return unless @metadata.activationEvents? - + subscribeToActivationCommands: -> @activationCommandSubscriptions = new CompositeDisposable - - if _.isArray(@metadata.activationEvents) - for eventName in @metadata.activationEvents + for selector, commands of @getActivationCommands() + for command in commands @activationCommandSubscriptions.add( - atom.commands.add('.workspace', eventName, @handleActivationEvent) - ) - else if _.isString(@metadata.activationEvents) - eventName = @metadata.activationEvents - @activationCommandSubscriptions.add( - atom.commands.add('.workspace', eventName, @handleActivationEvent) - ) - else - for eventName, selector of @metadata.activationEvents - selector ?= '.workspace' - @activationCommandSubscriptions.add( - atom.commands.add(selector, eventName, @handleActivationEvent) + atom.commands.add(selector, command, @handleActivationCommand) ) - handleActivationEvent: (event) => + getActivationCommands: -> + return @activationCommands if @activationCommands? + + @activationCommands = {} + + if @metadata.activationCommands? + for selector, commands of @metadata.activationCommands + @activationCommands[selector] ?= [] + if _.isString(commands) + @activationCommands[selector].push(commands) + else if _.isArray(commands) + @activationCommands[selector].push(commands...) + + if @metadata.activationEvents? + if _.isArray(@metadata.activationEvents) + for eventName in @metadata.activationEvents + @activationCommands['.workspace'] ?= [] + @activationCommands['.workspace'].push(eventName) + else if _.isString(@metadata.activationEvents) + eventName = @metadata.activationEvents + @activationCommands['.workspace'] ?= [] + @activationCommands['.workspace'].push(eventName) + else + for eventName, selector of @metadata.activationEvents + selector ?= '.workspace' + @activationCommands[selector] ?= [] + @activationCommands[selector].push(eventName) + + @activationCommands + + handleActivationCommand: (event) => event.stopImmediatePropagation() @activationCommandSubscriptions.dispose() reenableInvokedListeners = event.disableInvokedListeners() From 47f8f7eb11445818aba7744e1895ac56619a700f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 18:28:08 -0600 Subject: [PATCH 13/14] Switch specs to use activationCommands instead of activationEvents The activationEvents are converted to the same format as activationCommands, and that property will be deprecated. --- .../index.coffee | 11 +++++ .../package.cson | 5 +-- .../index.coffee | 16 ------- .../package.cson | 1 - .../index.coffee | 0 .../package.json | 2 +- spec/package-manager-spec.coffee | 43 ++++++------------- 7 files changed, 28 insertions(+), 50 deletions(-) delete mode 100644 spec/fixtures/packages/package-with-activation-events/index.coffee delete mode 100644 spec/fixtures/packages/package-with-activation-events/package.cson rename spec/fixtures/packages/{package-with-empty-activation-events => package-with-empty-activation-commands}/index.coffee (100%) rename spec/fixtures/packages/{package-with-empty-activation-events => package-with-empty-activation-commands}/package.json (53%) diff --git a/spec/fixtures/packages/package-with-activation-commands/index.coffee b/spec/fixtures/packages/package-with-activation-commands/index.coffee index d17894702..017058f64 100644 --- a/spec/fixtures/packages/package-with-activation-commands/index.coffee +++ b/spec/fixtures/packages/package-with-activation-commands/index.coffee @@ -1,2 +1,13 @@ module.exports = + activateCallCount: 0 + activationCommandCallCount: 0 + legacyActivationCommandCallCount: 0 + activate: -> + @activateCallCount++ + + atom.commands.add '.workspace', 'activation-command', => + @activationCommandCallCount++ + + atom.workspaceView.getActiveView()?.command 'activation-command', => + @legacyActivationCommandCallCount++ diff --git a/spec/fixtures/packages/package-with-activation-commands/package.cson b/spec/fixtures/packages/package-with-activation-commands/package.cson index ae14756ab..491f0a968 100644 --- a/spec/fixtures/packages/package-with-activation-commands/package.cson +++ b/spec/fixtures/packages/package-with-activation-commands/package.cson @@ -1,3 +1,2 @@ -activationCommands: - '.workspace': 'workspace-command' - '.editor': ['editor-command-1', 'editor-command-2'] +'activationCommands': + '.workspace': 'activation-command' diff --git a/spec/fixtures/packages/package-with-activation-events/index.coffee b/spec/fixtures/packages/package-with-activation-events/index.coffee deleted file mode 100644 index 3d7204dcb..000000000 --- a/spec/fixtures/packages/package-with-activation-events/index.coffee +++ /dev/null @@ -1,16 +0,0 @@ -class Foo - atom.deserializers.add(this) - @deserialize: ({data}) -> new Foo(data) - constructor: (@data) -> - -module.exports = - activateCallCount: 0 - activationEventCallCount: 0 - activationCommandCallCount: 0 - - activate: -> - @activateCallCount++ - atom.workspaceView.getActiveView()?.command 'activation-event', => - @activationEventCallCount++ - atom.commands.add '.workspace', 'activation-event', => - @activationCommandCallCount++ diff --git a/spec/fixtures/packages/package-with-activation-events/package.cson b/spec/fixtures/packages/package-with-activation-events/package.cson deleted file mode 100644 index dfa55c54d..000000000 --- a/spec/fixtures/packages/package-with-activation-events/package.cson +++ /dev/null @@ -1 +0,0 @@ -'activationEvents': ['activation-event'] diff --git a/spec/fixtures/packages/package-with-empty-activation-events/index.coffee b/spec/fixtures/packages/package-with-empty-activation-commands/index.coffee similarity index 100% rename from spec/fixtures/packages/package-with-empty-activation-events/index.coffee rename to spec/fixtures/packages/package-with-empty-activation-commands/index.coffee diff --git a/spec/fixtures/packages/package-with-empty-activation-events/package.json b/spec/fixtures/packages/package-with-empty-activation-commands/package.json similarity index 53% rename from spec/fixtures/packages/package-with-empty-activation-events/package.json rename to spec/fixtures/packages/package-with-empty-activation-commands/package.json index bae0844aa..1f1a1e343 100644 --- a/spec/fixtures/packages/package-with-empty-activation-events/package.json +++ b/spec/fixtures/packages/package-with-empty-activation-commands/package.json @@ -1,5 +1,5 @@ { "name": "no events", "version": "0.1.0", - "activationEvents": [] + "activationCommands": {".workspace": []} } diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index d9a0e5828..bb1f1f048 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -92,25 +92,25 @@ describe "PackageManager", -> expect(atom.config.get('package-with-config-defaults.numbers.one')).toBe 1 expect(atom.config.get('package-with-config-defaults.numbers.two')).toBe 2 - describe "when the package metadata includes `activationEvents`", -> + describe "when the package metadata includes `activationCommands`", -> [mainModule, promise, workspaceCommandListener] = [] beforeEach -> atom.workspaceView.attachToDom() - mainModule = require './fixtures/packages/package-with-activation-events/index' - mainModule.activationEventCallCount = 0 + mainModule = require './fixtures/packages/package-with-activation-commands/index' + mainModule.legacyActivationCommandCallCount = 0 mainModule.activationCommandCallCount = 0 spyOn(mainModule, 'activate').andCallThrough() spyOn(Package.prototype, 'requireMainModule').andCallThrough() workspaceCommandListener = jasmine.createSpy('workspaceCommandListener') - atom.commands.add '.workspace', 'activation-event', workspaceCommandListener + atom.commands.add '.workspace', 'activation-command', workspaceCommandListener - promise = atom.packages.activatePackage('package-with-activation-events') + promise = atom.packages.activatePackage('package-with-activation-commands') it "defers requiring/activating the main module until an activation event bubbles to the root view", -> expect(promise.isFulfilled()).not.toBeTruthy() - atom.workspaceView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) + atom.workspaceView[0].dispatchEvent(new CustomEvent('activation-command', bubbles: true)) waitsForPromise -> promise @@ -122,18 +122,18 @@ describe "PackageManager", -> runs -> editorView = atom.workspaceView.getActiveView() legacyCommandListener = jasmine.createSpy("legacyCommandListener") - editorView.command 'activation-event', legacyCommandListener + editorView.command 'activation-command', legacyCommandListener editorCommandListener = jasmine.createSpy("editorCommandListener") - atom.commands.add '.editor', 'activation-event', editorCommandListener - editorView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) + atom.commands.add '.editor', 'activation-command', editorCommandListener + editorView[0].dispatchEvent(new CustomEvent('activation-command', bubbles: true)) expect(mainModule.activate.callCount).toBe 1 - expect(mainModule.activationEventCallCount).toBe 1 + expect(mainModule.legacyActivationCommandCallCount).toBe 1 expect(mainModule.activationCommandCallCount).toBe 1 expect(legacyCommandListener.callCount).toBe 1 expect(editorCommandListener.callCount).toBe 1 expect(workspaceCommandListener.callCount).toBe 1 - editorView[0].dispatchEvent(new CustomEvent('activation-event', bubbles: true)) - expect(mainModule.activationEventCallCount).toBe 2 + editorView[0].dispatchEvent(new CustomEvent('activation-command', bubbles: true)) + expect(mainModule.legacyActivationCommandCallCount).toBe 2 expect(mainModule.activationCommandCallCount).toBe 2 expect(legacyCommandListener.callCount).toBe 2 expect(editorCommandListener.callCount).toBe 2 @@ -141,30 +141,15 @@ describe "PackageManager", -> expect(mainModule.activate.callCount).toBe 1 it "activates the package immediately when the events are empty", -> - mainModule = require './fixtures/packages/package-with-empty-activation-events/index' + mainModule = require './fixtures/packages/package-with-empty-activation-commands/index' spyOn(mainModule, 'activate').andCallThrough() waitsForPromise -> - atom.packages.activatePackage('package-with-empty-activation-events') + atom.packages.activatePackage('package-with-empty-activation-commands') runs -> expect(mainModule.activate.callCount).toBe 1 - describe "when the package metadata includes `activationCommands`", -> - it "defers activation until one of the commands is invoked", -> - atom.workspaceView.attachToDom() - mainModule = require './fixtures/packages/package-with-activation-commands/index' - mainModule.commands = [] - spyOn(mainModule, 'activate').andCallThrough() - spyOn(Package.prototype, 'requireMainModule').andCallThrough() - - promise = atom.packages.activatePackage('package-with-activation-commands') - expect(promise.isFulfilled()).not.toBeTruthy() - - atom.workspaceView[0].dispatchEvent(new CustomEvent('workspace-command', bubbles: true)) - - waitsForPromise -> promise - describe "when the package has no main module", -> it "does not throw an exception", -> spyOn(console, "error") From b7765d9416cc4865fc7abc0e582306adb337c5d8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Sep 2014 19:15:18 -0600 Subject: [PATCH 14/14] Process commands invoked with jQuery trigger in CommandRegistry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Especially in specs, trigger has been used to invoke events. jQuery does not invoke native listeners in this situation, so we use ::on to listen for them instead. If we didn’t handle the event with a native capture handler, we’ll still support invoking via trigger. --- src/command-registry.coffee | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 800b48077..abeaa155e 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -53,8 +53,8 @@ class CommandRegistry @rootNode = newRootNode for commandName of @listenersByCommandName - oldRootNode?.removeEventListener(commandName, @handleCommandEvent, true) - newRootNode?.addEventListener(commandName, @handleCommandEvent, true) + @removeCommandListener(oldRootNode, commandName) + @addCommandListener(newRootNode, commandName) # Public: Add one or more command listeners associated with a selector. # @@ -90,7 +90,7 @@ class CommandRegistry return disposable unless @listenersByCommandName[commandName]? - @rootNode?.addEventListener(commandName, @handleCommandEvent, true) + @addCommandListener(@rootNode, commandName) @listenersByCommandName[commandName] = [] listener = new CommandListener(selector, callback) @@ -101,7 +101,7 @@ class CommandRegistry listenersForCommand.splice(listenersForCommand.indexOf(listener), 1) if listenersForCommand.length is 0 delete @listenersByCommandName[commandName] - @rootNode.removeEventListener(commandName, @handleCommandEvent, true) + @removeCommandListener(@rootNode, commandName) # Public: Find all registered commands matching a query. # @@ -161,6 +161,8 @@ class CommandRegistry @setRootNode(rootNode) # restore listeners for commands in snapshot handleCommandEvent: (originalEvent) => + originalEvent.__handledByCommandRegistry = true + propagationStopped = false immediatePropagationStopped = false matched = false @@ -201,6 +203,17 @@ class CommandRegistry matched + handleJQueryCommandEvent: (event) => + @handleCommandEvent(event) unless event.originalEvent?.__handledByCommandRegistry + + addCommandListener: (node, commandName, listener) -> + node?.addEventListener(commandName, @handleCommandEvent, true) + $(node).on commandName, @handleJQueryCommandEvent + + removeCommandListener: (node, commandName) -> + node?.removeEventListener(commandName, @handleCommandEvent, true) + $(node).off commandName, @handleJQueryCommandEvent + class CommandListener enabled: true