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/command-registry-spec.coffee b/spec/command-registry-spec.coffee index 8985e075b..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 = [] @@ -124,3 +140,52 @@ 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 + + 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 + + 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/fixtures/packages/package-with-activation-commands/index.coffee b/spec/fixtures/packages/package-with-activation-commands/index.coffee new file mode 100644 index 000000000..017058f64 --- /dev/null +++ b/spec/fixtures/packages/package-with-activation-commands/index.coffee @@ -0,0 +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 new file mode 100644 index 000000000..491f0a968 --- /dev/null +++ b/spec/fixtures/packages/package-with-activation-commands/package.cson @@ -0,0 +1,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 fd91e6f79..000000000 --- a/spec/fixtures/packages/package-with-activation-events/index.coffee +++ /dev/null @@ -1,13 +0,0 @@ -class Foo - atom.deserializers.add(this) - @deserialize: ({data}) -> new Foo(data) - constructor: (@data) -> - -module.exports = - activateCallCount: 0 - activationEventCallCount: 0 - - activate: -> - @activateCallCount++ - atom.workspaceView.getActiveView()?.command 'activation-event', => - @activationEventCallCount++ 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 new file mode 100644 index 000000000..bb1f1f048 --- /dev/null +++ b/spec/package-manager-spec.coffee @@ -0,0 +1,555 @@ +{$, $$, 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 `activationCommands`", -> + [mainModule, promise, workspaceCommandListener] = [] + + beforeEach -> + atom.workspaceView.attachToDom() + 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-command', workspaceCommandListener + + 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-command', bubbles: true)) + + waitsForPromise -> + promise + + it "triggers the activation event on all handlers registered during activation", -> + waitsForPromise -> + atom.workspaceView.open() + + runs -> + editorView = atom.workspaceView.getActiveView() + legacyCommandListener = jasmine.createSpy("legacyCommandListener") + editorView.command 'activation-command', legacyCommandListener + editorCommandListener = jasmine.createSpy("editorCommandListener") + atom.commands.add '.editor', 'activation-command', editorCommandListener + editorView[0].dispatchEvent(new CustomEvent('activation-command', bubbles: true)) + expect(mainModule.activate.callCount).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-command', bubbles: true)) + expect(mainModule.legacyActivationCommandCallCount).toBe 2 + expect(mainModule.activationCommandCallCount).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", -> + mainModule = require './fixtures/packages/package-with-empty-activation-commands/index' + spyOn(mainModule, 'activate').andCallThrough() + + waitsForPromise -> + atom.packages.activatePackage('package-with-empty-activation-commands') + + 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 diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 48fad5c8c..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 = {} @@ -121,8 +123,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 23a54fea2..abeaa155e 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -46,13 +46,15 @@ class CommandRegistry constructor: (@rootNode) -> @listenersByCommandName = {} + getRootNode: -> @rootNode + setRootNode: (newRootNode) -> oldRootNode = @rootNode @rootNode = newRootNode for commandName of @listenersByCommandName - oldRootNode?.removeEventListener(commandName, @dispatchCommand, true) - newRootNode?.addEventListener(commandName, @dispatchCommand, true) + @removeCommandListener(oldRootNode, commandName) + @addCommandListener(newRootNode, commandName) # Public: Add one or more command listeners associated with a selector. # @@ -88,7 +90,7 @@ class CommandRegistry return disposable unless @listenersByCommandName[commandName]? - @rootNode?.addEventListener(commandName, @dispatchCommand, true) + @addCommandListener(@rootNode, commandName) @listenersByCommandName[commandName] = [] listener = new CommandListener(selector, callback) @@ -99,35 +101,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 + @removeCommandListener(@rootNode, commandName) # Public: Find all registered commands matching a query. # @@ -163,10 +137,86 @@ class CommandRegistry commands - clear: -> - @listenersByCommandName = {} + # 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) + + 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: (originalEvent) => + originalEvent.__handledByCommandRegistry = true + + propagationStopped = false + immediatePropagationStopped = false + matched = false + currentTarget = originalEvent.target + invokedListeners = [] + + 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 + disableInvokedListeners: value: -> + listener.enabled = false for listener in invokedListeners + -> listener.enabled = true for listener in invokedListeners + + loop + matchingListeners = + (@listenersByCommandName[originalEvent.type] ? []) + .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) + .sort (a, b) -> a.compare(b) + + matched = true if matchingListeners.length > 0 + + for listener in matchingListeners when listener.enabled + break if immediatePropagationStopped + invokedListeners.push(listener) + listener.callback.call(currentTarget, syntheticEvent) + + break unless currentTarget? + break if currentTarget is @rootNode + break if propagationStopped + currentTarget = currentTarget.parentNode + + 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 + constructor: (@selector, @callback) -> @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) @sequenceNumber = SequenceCount++ diff --git a/src/package.coffee b/src/package.coffee index c005ae664..30cd1ad2f 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' @@ -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() @@ -270,7 +270,7 @@ class Package deactivate: -> @activationDeferred?.reject() @activationDeferred = null - @unsubscribeFromActivationEvents() + @activationCommandSubscriptions?.dispose() @deactivateResources() @deactivateConfig() if @mainActivated @@ -319,64 +319,56 @@ 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? - if _.isArray(@metadata.activationEvents) - atom.workspaceView.command(event, @handleActivationEvent) for event in @metadata.activationEvents - else if _.isString(@metadata.activationEvents) - atom.workspaceView.command(@metadata.activationEvents, @handleActivationEvent) - else - atom.workspaceView.command(event, selector, @handleActivationEvent) for event, selector of @metadata.activationEvents + subscribeToActivationCommands: -> + @activationCommandSubscriptions = new CompositeDisposable + for selector, commands of @getActivationCommands() + for command in commands + @activationCommandSubscriptions.add( + atom.commands.add(selector, command, @handleActivationCommand) + ) - handleActivationEvent: (event) => - bubblePathEventHandlers = @disableEventHandlersOnBubblePath(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() @activateNow() - $ ?= require('./space-pen-extensions').$ - $(event.target).trigger(event) - @restoreEventHandlersOnBubblePath(bubblePathEventHandlers) - @unsubscribeFromActivationEvents() - false - - 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 + event.target.dispatchEvent(new CustomEvent(event.type, bubbles: true)) + reenableInvokedListeners() # Does the given module path contain native code? isNativeModule: (modulePath) ->