mirror of
https://github.com/atom/atom.git
synced 2026-02-12 07:35:14 -05:00
This was bootstrapped with running `decaffeinate` on both command-registry and its spec, but involved a line-by-line eyeing and updating for idioms and, in one case, correctness: For some reason, copying the keys to the new dispatchedEvent in coffeescript (command-registry.coffee:235) currently does not result in a thrown exception, as it should when trying to write over the read-only property `isTrusted`, so I added a check to ensure that the key did not already exist in the new event. Moreover, other definitions of `preventDefault`, `stopPropagation` and friends, which are also enumerable afaict, would also overwrite the ones defined just prior (command-registry.coffee:220 and command-registry.js:325), so that check ensures we don't overwrite those either. Test plan: `./script/lint && ./script/test`, and a lengthy smoke test of various commands through keybindings, command palette, and package interfaces. Released under CC0.
346 lines
13 KiB
JavaScript
346 lines
13 KiB
JavaScript
const CommandRegistry = require('../src/command-registry');
|
|
const _ = require('underscore-plus');
|
|
|
|
describe("CommandRegistry", () => {
|
|
let registry, parent, child, grandchild;
|
|
|
|
beforeEach(() => {
|
|
parent = document.createElement("div");
|
|
child = document.createElement("div");
|
|
grandchild = document.createElement("div");
|
|
parent.classList.add('parent');
|
|
child.classList.add('child');
|
|
grandchild.classList.add('grandchild');
|
|
child.appendChild(grandchild);
|
|
parent.appendChild(child);
|
|
document.querySelector('#jasmine-content').appendChild(parent);
|
|
|
|
registry = new CommandRegistry;
|
|
registry.attach(parent);
|
|
});
|
|
|
|
afterEach(() => registry.destroy());
|
|
|
|
describe("when a command event is dispatched on an element", () => {
|
|
it("invokes callbacks with selectors matching the target", () => {
|
|
let called = false;
|
|
registry.add('.grandchild', 'command', function (event) {
|
|
expect(this).toBe(grandchild);
|
|
expect(event.type).toBe('command');
|
|
expect(event.eventPhase).toBe(Event.BUBBLING_PHASE);
|
|
expect(event.target).toBe(grandchild);
|
|
expect(event.currentTarget).toBe(grandchild);
|
|
called = true;
|
|
});
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', {bubbles: true}));
|
|
expect(called).toBe(true);
|
|
});
|
|
|
|
it("invokes callbacks with selectors matching ancestors of the target", () => {
|
|
const calls = [];
|
|
|
|
registry.add('.child', 'command', function (event) {
|
|
expect(this).toBe(child);
|
|
expect(event.target).toBe(grandchild);
|
|
expect(event.currentTarget).toBe(child);
|
|
calls.push('child');
|
|
});
|
|
|
|
registry.add('.parent', 'command', function (event) {
|
|
expect(this).toBe(parent);
|
|
expect(event.target).toBe(grandchild);
|
|
expect(event.currentTarget).toBe(parent);
|
|
calls.push('parent');
|
|
});
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', {bubbles: true}));
|
|
expect(calls).toEqual(['child', 'parent']);
|
|
});
|
|
|
|
it("invokes inline listeners prior to listeners applied via selectors", () => {
|
|
const calls = [];
|
|
registry.add('.grandchild', 'command', () => calls.push('grandchild'));
|
|
registry.add(child, 'command', () => calls.push('child-inline'));
|
|
registry.add('.child', 'command', () => calls.push('child'));
|
|
registry.add('.parent', 'command', () => calls.push('parent'));
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', {bubbles: true}));
|
|
expect(calls).toEqual(['grandchild', 'child-inline', 'child', 'parent']);
|
|
});
|
|
|
|
it("orders multiple matching listeners for an element by selector specificity", () => {
|
|
child.classList.add('foo', 'bar');
|
|
const calls = [];
|
|
|
|
registry.add('.foo.bar', 'command', () => calls.push('.foo.bar'));
|
|
registry.add('.foo', 'command', () => calls.push('.foo'));
|
|
registry.add('.bar', 'command', () => calls.push('.bar')); // specificity ties favor commands added later, like CSS
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', {bubbles: true}));
|
|
expect(calls).toEqual(['.foo.bar', '.bar', '.foo']);
|
|
});
|
|
|
|
it("orders inline listeners by reverse registration order", () => {
|
|
const calls = [];
|
|
registry.add(child, 'command', () => calls.push('child1'));
|
|
registry.add(child, 'command', () => calls.push('child2'));
|
|
child.dispatchEvent(new CustomEvent('command', {bubbles: true}));
|
|
expect(calls).toEqual(['child2', 'child1']);
|
|
});
|
|
|
|
it("stops bubbling through ancestors when .stopPropagation() is called on the event", () => {
|
|
const calls = [];
|
|
|
|
registry.add('.parent', 'command', () => calls.push('parent'));
|
|
registry.add('.child', 'command', () => calls.push('child-2'));
|
|
registry.add('.child', 'command', (event) => {
|
|
calls.push('child-1');
|
|
event.stopPropagation();
|
|
});
|
|
|
|
const dispatchedEvent = new CustomEvent('command', {bubbles: true});
|
|
spyOn(dispatchedEvent, 'stopPropagation');
|
|
grandchild.dispatchEvent(dispatchedEvent);
|
|
expect(calls).toEqual(['child-1', 'child-2']);
|
|
expect(dispatchedEvent.stopPropagation).toHaveBeenCalled();
|
|
});
|
|
|
|
it("stops invoking callbacks when .stopImmediatePropagation() is called on the event", () => {
|
|
const calls = [];
|
|
|
|
registry.add('.parent', 'command', () => calls.push('parent'));
|
|
registry.add('.child', 'command', () => calls.push('child-2'));
|
|
registry.add('.child', 'command', (event) => {
|
|
calls.push('child-1');
|
|
event.stopImmediatePropagation();
|
|
});
|
|
|
|
const dispatchedEvent = new CustomEvent('command', {bubbles: true});
|
|
spyOn(dispatchedEvent, 'stopImmediatePropagation');
|
|
grandchild.dispatchEvent(dispatchedEvent);
|
|
expect(calls).toEqual(['child-1']);
|
|
expect(dispatchedEvent.stopImmediatePropagation).toHaveBeenCalled();
|
|
});
|
|
|
|
it("forwards .preventDefault() calls from the synthetic event to the original", () => {
|
|
registry.add('.child', 'command', event => event.preventDefault());
|
|
|
|
const dispatchedEvent = new CustomEvent('command', {bubbles: true});
|
|
spyOn(dispatchedEvent, 'preventDefault');
|
|
grandchild.dispatchEvent(dispatchedEvent);
|
|
expect(dispatchedEvent.preventDefault).toHaveBeenCalled();
|
|
});
|
|
|
|
it("forwards .abortKeyBinding() calls from the synthetic event to the original", () => {
|
|
registry.add('.child', 'command', event => event.abortKeyBinding());
|
|
|
|
const dispatchedEvent = new CustomEvent('command', {bubbles: true});
|
|
dispatchedEvent.abortKeyBinding = jasmine.createSpy('abortKeyBinding');
|
|
grandchild.dispatchEvent(dispatchedEvent);
|
|
expect(dispatchedEvent.abortKeyBinding).toHaveBeenCalled();
|
|
});
|
|
|
|
it("copies non-standard properties from the original event to the synthetic event", () => {
|
|
let syntheticEvent = null;
|
|
registry.add('.child', 'command', event => syntheticEvent = event);
|
|
|
|
const dispatchedEvent = new CustomEvent('command', {bubbles: true});
|
|
dispatchedEvent.nonStandardProperty = 'testing';
|
|
grandchild.dispatchEvent(dispatchedEvent);
|
|
expect(syntheticEvent.nonStandardProperty).toBe('testing');
|
|
});
|
|
|
|
it("allows listeners to be removed via a disposable returned by ::add", () => {
|
|
let calls = [];
|
|
|
|
const disposable1 = registry.add('.parent', 'command', () => calls.push('parent'));
|
|
const disposable2 = registry.add('.child', 'command', () => calls.push('child'));
|
|
|
|
disposable1.dispose();
|
|
grandchild.dispatchEvent(new CustomEvent('command', {bubbles: true}));
|
|
expect(calls).toEqual(['child']);
|
|
|
|
calls = [];
|
|
disposable2.dispose();
|
|
grandchild.dispatchEvent(new CustomEvent('command', {bubbles: true}));
|
|
expect(calls).toEqual([]);
|
|
});
|
|
|
|
it("allows multiple commands to be registered under one selector when called with an object", () => {
|
|
let calls = [];
|
|
|
|
const disposable = registry.add('.child', {
|
|
'command-1'() {
|
|
calls.push('command-1');
|
|
},
|
|
'command-2'() {
|
|
calls.push('command-2');
|
|
}
|
|
});
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command-1', {bubbles: true}));
|
|
grandchild.dispatchEvent(new CustomEvent('command-2', {bubbles: true}));
|
|
|
|
expect(calls).toEqual(['command-1', 'command-2']);
|
|
|
|
calls = [];
|
|
disposable.dispose();
|
|
grandchild.dispatchEvent(new CustomEvent('command-1', {bubbles: true}));
|
|
grandchild.dispatchEvent(new CustomEvent('command-2', {bubbles: true}));
|
|
expect(calls).toEqual([]);
|
|
});
|
|
|
|
it("invokes callbacks registered with ::onWillDispatch and ::onDidDispatch", () => {
|
|
const sequence = [];
|
|
|
|
registry.onDidDispatch(event => sequence.push(['onDidDispatch', event]));
|
|
|
|
registry.add('.grandchild', 'command', event => sequence.push(['listener', event]));
|
|
|
|
registry.onWillDispatch(event => sequence.push(['onWillDispatch', event]));
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command', {bubbles: true}));
|
|
|
|
expect(sequence[0][0]).toBe('onWillDispatch');
|
|
expect(sequence[1][0]).toBe('listener');
|
|
expect(sequence[2][0]).toBe('onDidDispatch');
|
|
|
|
expect(sequence[0][1] === sequence[1][1] && sequence[1][1] === sequence[2][1]).toBe(true);
|
|
expect(sequence[0][1].constructor).toBe(CustomEvent);
|
|
expect(sequence[0][1].target).toBe(grandchild);
|
|
});
|
|
});
|
|
|
|
describe("::add(selector, commandName, callback)", () => {
|
|
it("throws an error when called with an invalid selector", () => {
|
|
const badSelector = '<>';
|
|
let addError = null;
|
|
try {
|
|
registry.add(badSelector, 'foo:bar', () => {});
|
|
} catch (error) {
|
|
addError = error;
|
|
}
|
|
expect(addError.message).toContain(badSelector);
|
|
});
|
|
|
|
it("throws an error when called with a non-function callback and selector target", () => {
|
|
const badCallback = null;
|
|
let addError = null;
|
|
|
|
try {
|
|
registry.add('.selector', 'foo:bar', badCallback);
|
|
} catch (error) {
|
|
addError = error;
|
|
}
|
|
expect(addError.message).toContain("Can't register a command with non-function callback.");
|
|
});
|
|
|
|
it("throws an error when called with an non-function callback and object target", () => {
|
|
const badCallback = null;
|
|
let addError = null;
|
|
|
|
try {
|
|
registry.add(document.body, 'foo:bar', badCallback);
|
|
} catch (error) {
|
|
addError = error;
|
|
}
|
|
expect(addError.message).toContain("Can't register a command with non-function callback.");
|
|
});
|
|
});
|
|
|
|
describe("::findCommands({target})", () =>
|
|
it("returns commands that can be invoked on the target or its ancestors", () => {
|
|
registry.add('.parent', 'namespace:command-1', () => {});
|
|
registry.add('.child', 'namespace:command-2', () => {});
|
|
registry.add('.grandchild', 'namespace:command-3', () => {});
|
|
registry.add('.grandchild.no-match', 'namespace:command-4', () => {});
|
|
|
|
registry.add(grandchild, 'namespace:inline-command-1', () => {});
|
|
registry.add(child, 'namespace:inline-command-2', () => {});
|
|
|
|
const commands = registry.findCommands({target: grandchild});
|
|
const nonJqueryCommands = _.reject(commands, cmd => cmd.jQuery);
|
|
expect(nonJqueryCommands).toEqual([
|
|
{name: 'namespace:inline-command-1', displayName: 'Namespace: Inline Command 1'},
|
|
{name: 'namespace:command-3', displayName: 'Namespace: Command 3'},
|
|
{name: 'namespace:inline-command-2', displayName: 'Namespace: Inline Command 2'},
|
|
{name: 'namespace:command-2', displayName: 'Namespace: Command 2'},
|
|
{name: 'namespace:command-1', displayName: 'Namespace: Command 1'}
|
|
]);
|
|
})
|
|
);
|
|
|
|
describe("::dispatch(target, commandName)", () => {
|
|
it("simulates invocation of the given command ", () => {
|
|
let called = false;
|
|
registry.add('.grandchild', 'command', function (event) {
|
|
expect(this).toBe(grandchild);
|
|
expect(event.type).toBe('command');
|
|
expect(event.eventPhase).toBe(Event.BUBBLING_PHASE);
|
|
expect(event.target).toBe(grandchild);
|
|
expect(event.currentTarget).toBe(grandchild);
|
|
called = true;
|
|
});
|
|
|
|
registry.dispatch(grandchild, 'command');
|
|
expect(called).toBe(true);
|
|
});
|
|
|
|
it("returns a 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', () => {});
|
|
const snapshot = registry.getSnapshot();
|
|
registry.add('.grandchild', 'namespace:command-3', () => {});
|
|
|
|
expect(registry.findCommands({target: grandchild}).slice(0, 3)).toEqual([
|
|
{name: 'namespace:command-3', displayName: 'Namespace: Command 3'},
|
|
{name: 'namespace:command-2', displayName: 'Namespace: Command 2'},
|
|
{name: 'namespace:command-1', displayName: 'Namespace: Command 1'}
|
|
]);
|
|
|
|
registry.restoreSnapshot(snapshot);
|
|
|
|
expect(registry.findCommands({target: grandchild}).slice(0, 2)).toEqual([
|
|
{name: 'namespace:command-2', displayName: 'Namespace: Command 2'},
|
|
{name: 'namespace:command-1', displayName: 'Namespace: Command 1'}
|
|
]);
|
|
|
|
registry.add('.grandchild', 'namespace:command-3', () => {});
|
|
registry.restoreSnapshot(snapshot);
|
|
|
|
expect(registry.findCommands({target: grandchild}).slice(0, 2)).toEqual([
|
|
{name: 'namespace:command-2', displayName: 'Namespace: Command 2'},
|
|
{name: 'namespace:command-1', displayName: 'Namespace: Command 1'}
|
|
]);
|
|
})
|
|
);
|
|
|
|
describe("::attach(rootNode)", () =>
|
|
it("adds event listeners for any previously-added commands", () => {
|
|
const registry2 = new CommandRegistry;
|
|
|
|
const commandSpy = jasmine.createSpy('command-callback');
|
|
registry2.add('.grandchild', 'command-1', commandSpy);
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command-1', {bubbles: true}));
|
|
expect(commandSpy).not.toHaveBeenCalled();
|
|
|
|
registry2.attach(parent);
|
|
|
|
grandchild.dispatchEvent(new CustomEvent('command-1', {bubbles: true}));
|
|
expect(commandSpy).toHaveBeenCalled();
|
|
})
|
|
);
|
|
});
|