describe('Config', () => { let savedSettings; beforeEach(() => { spyOn(console, 'warn'); atom.config.settingsLoaded = true; savedSettings = []; atom.config.saveCallback = function(settings) { savedSettings.push(settings); }; }); describe('.get(keyPath, {scope, sources, excludeSources})', () => { it("allows a key path's value to be read", () => { expect(atom.config.set('foo.bar.baz', 42)).toBe(true); expect(atom.config.get('foo.bar.baz')).toBe(42); expect(atom.config.get('foo.quux')).toBeUndefined(); }); it("returns a deep clone of the key path's value", () => { atom.config.set('value', { array: [1, { b: 2 }, 3] }); const retrievedValue = atom.config.get('value'); retrievedValue.array[0] = 4; retrievedValue.array[1].b = 2.1; expect(atom.config.get('value')).toEqual({ array: [1, { b: 2 }, 3] }); }); it('merges defaults into the returned value if both the assigned value and the default value are objects', () => { atom.config.setDefaults('foo.bar', { baz: 1, ok: 2 }); atom.config.set('foo.bar', { baz: 3 }); expect(atom.config.get('foo.bar')).toEqual({ baz: 3, ok: 2 }); atom.config.setDefaults('other', { baz: 1 }); atom.config.set('other', 7); expect(atom.config.get('other')).toBe(7); atom.config.set('bar.baz', { a: 3 }); atom.config.setDefaults('bar', { baz: 7 }); expect(atom.config.get('bar.baz')).toEqual({ a: 3 }); }); describe("when a 'sources' option is specified", () => it('only retrieves values from the specified sources', () => { atom.config.set('x.y', 1, { scopeSelector: '.foo', source: 'a' }); atom.config.set('x.y', 2, { scopeSelector: '.foo', source: 'b' }); atom.config.set('x.y', 3, { scopeSelector: '.foo', source: 'c' }); atom.config.setSchema('x.y', { type: 'integer', default: 4 }); expect( atom.config.get('x.y', { sources: ['a'], scope: ['.foo'] }) ).toBe(1); expect( atom.config.get('x.y', { sources: ['b'], scope: ['.foo'] }) ).toBe(2); expect( atom.config.get('x.y', { sources: ['c'], scope: ['.foo'] }) ).toBe(3); // Schema defaults never match a specific source. We could potentially add a special "schema" source. expect( atom.config.get('x.y', { sources: ['x'], scope: ['.foo'] }) ).toBeUndefined(); expect( atom.config.get(null, { sources: ['a'], scope: ['.foo'] }).x.y ).toBe(1); })); describe("when an 'excludeSources' option is specified", () => it('only retrieves values from the specified sources', () => { atom.config.set('x.y', 0); atom.config.set('x.y', 1, { scopeSelector: '.foo', source: 'a' }); atom.config.set('x.y', 2, { scopeSelector: '.foo', source: 'b' }); atom.config.set('x.y', 3, { scopeSelector: '.foo', source: 'c' }); atom.config.setSchema('x.y', { type: 'integer', default: 4 }); expect( atom.config.get('x.y', { excludeSources: ['a'], scope: ['.foo'] }) ).toBe(3); expect( atom.config.get('x.y', { excludeSources: ['c'], scope: ['.foo'] }) ).toBe(2); expect( atom.config.get('x.y', { excludeSources: ['b', 'c'], scope: ['.foo'] }) ).toBe(1); expect( atom.config.get('x.y', { excludeSources: ['b', 'c', 'a'], scope: ['.foo'] }) ).toBe(0); expect( atom.config.get('x.y', { excludeSources: ['b', 'c', 'a', atom.config.getUserConfigPath()], scope: ['.foo'] }) ).toBe(4); expect( atom.config.get('x.y', { excludeSources: [atom.config.getUserConfigPath()] }) ).toBe(4); })); describe("when a 'scope' option is given", () => { it('returns the property with the most specific scope selector', () => { atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.double.coffee' }); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source .string.quoted.double' }); atom.config.set('foo.bar.baz', 11, { scopeSelector: '.source' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }) ).toBe(42); expect( atom.config.get('foo.bar.baz', { scope: ['.source.js', '.string.quoted.double.js'] }) ).toBe(22); expect( atom.config.get('foo.bar.baz', { scope: ['.source.js', '.variable.assignment.js'] }) ).toBe(11); expect( atom.config.get('foo.bar.baz', { scope: ['.text'] }) ).toBeUndefined(); }); it('favors the most recently added properties in the event of a specificity tie', () => { atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.single' }); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source.coffee .string.quoted.double' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.single'] }) ).toBe(42); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.single.double'] }) ).toBe(22); }); describe('when there are global defaults', () => it('falls back to the global when there is no scoped property specified', () => { atom.config.setDefaults('foo', { hasDefault: 'ok' }); expect( atom.config.get('foo.hasDefault', { scope: ['.source.coffee', '.string.quoted.single'] }) ).toBe('ok'); })); describe('when package settings are added after user settings', () => it("returns the user's setting because the user's setting has higher priority", () => { atom.config.set('foo.bar.baz', 100, { scopeSelector: '.source.coffee' }); atom.config.set('foo.bar.baz', 1, { scopeSelector: '.source.coffee', source: 'some-package' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(100); })); }); }); describe('.getAll(keyPath, {scope, sources, excludeSources})', () => { it('reads all of the values for a given key-path', () => { expect(atom.config.set('foo', 41)).toBe(true); expect(atom.config.set('foo', 43, { scopeSelector: '.a .b' })).toBe(true); expect(atom.config.set('foo', 42, { scopeSelector: '.a' })).toBe(true); expect(atom.config.set('foo', 44, { scopeSelector: '.a .b.c' })).toBe( true ); expect(atom.config.set('foo', -44, { scopeSelector: '.d' })).toBe(true); expect(atom.config.getAll('foo', { scope: ['.a', '.b.c'] })).toEqual([ { scopeSelector: '.a .b.c', value: 44 }, { scopeSelector: '.a .b', value: 43 }, { scopeSelector: '.a', value: 42 }, { scopeSelector: '*', value: 41 } ]); }); it("includes the schema's default value", () => { atom.config.setSchema('foo', { type: 'number', default: 40 }); expect(atom.config.set('foo', 43, { scopeSelector: '.a .b' })).toBe(true); expect(atom.config.getAll('foo', { scope: ['.a', '.b.c'] })).toEqual([ { scopeSelector: '.a .b', value: 43 }, { scopeSelector: '*', value: 40 } ]); }); }); describe('.set(keyPath, value, {source, scopeSelector})', () => { it("allows a key path's value to be written", () => { expect(atom.config.set('foo.bar.baz', 42)).toBe(true); expect(atom.config.get('foo.bar.baz')).toBe(42); }); it("saves the user's config to disk after it stops changing", () => { atom.config.set('foo.bar.baz', 42); expect(savedSettings.length).toBe(0); atom.config.set('foo.bar.baz', 43); expect(savedSettings.length).toBe(0); atom.config.set('foo.bar.baz', 44); advanceClock(10); expect(savedSettings.length).toBe(1); }); it("does not save when a non-default 'source' is given", () => { atom.config.set('foo.bar.baz', 42, { source: 'some-other-source', scopeSelector: '.a' }); advanceClock(500); expect(savedSettings.length).toBe(0); }); it("does not allow a 'source' option without a 'scopeSelector'", () => { expect(() => atom.config.set('foo', 1, { source: ['.source.ruby'] }) ).toThrow(); }); describe('when the key-path is null', () => it('sets the root object', () => { expect(atom.config.set(null, { editor: { tabLength: 6 } })).toBe(true); expect(atom.config.get('editor.tabLength')).toBe(6); expect( atom.config.set(null, { editor: { tabLength: 8, scopeSelector: ['.source.js'] } }) ).toBe(true); expect( atom.config.get('editor.tabLength', { scope: ['.source.js'] }) ).toBe(8); })); describe('when the value equals the default value', () => it("does not store the value in the user's config", () => { atom.config.setSchema('foo', { type: 'object', properties: { same: { type: 'number', default: 1 }, changes: { type: 'number', default: 1 }, sameArray: { type: 'array', default: [1, 2, 3] }, sameObject: { type: 'object', default: { a: 1, b: 2 } }, null: { type: '*', default: null }, undefined: { type: '*', default: undefined } } }); expect(atom.config.settings.foo).toBeUndefined(); atom.config.set('foo.same', 1); atom.config.set('foo.changes', 2); atom.config.set('foo.sameArray', [1, 2, 3]); atom.config.set('foo.null', undefined); atom.config.set('foo.undefined', null); atom.config.set('foo.sameObject', { b: 2, a: 1 }); const userConfigPath = atom.config.getUserConfigPath(); expect( atom.config.get('foo.same', { sources: [userConfigPath] }) ).toBeUndefined(); expect(atom.config.get('foo.changes')).toBe(2); expect( atom.config.get('foo.changes', { sources: [userConfigPath] }) ).toBe(2); atom.config.set('foo.changes', 1); expect( atom.config.get('foo.changes', { sources: [userConfigPath] }) ).toBeUndefined(); })); describe("when a 'scopeSelector' is given", () => it('sets the value and overrides the others', () => { atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.double.coffee' }); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source .string.quoted.double' }); atom.config.set('foo.bar.baz', 11, { scopeSelector: '.source' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }) ).toBe(42); expect( atom.config.set('foo.bar.baz', 100, { scopeSelector: '.source.coffee .string.quoted.double.coffee' }) ).toBe(true); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }) ).toBe(100); })); }); describe('.unset(keyPath, {source, scopeSelector})', () => { beforeEach(() => atom.config.setSchema('foo', { type: 'object', properties: { bar: { type: 'object', properties: { baz: { type: 'integer', default: 0 }, ok: { type: 'integer', default: 0 } } }, quux: { type: 'integer', default: 0 } } }) ); it('sets the value of the key path to its default', () => { atom.config.setDefaults('a', { b: 3 }); atom.config.set('a.b', 4); expect(atom.config.get('a.b')).toBe(4); atom.config.unset('a.b'); expect(atom.config.get('a.b')).toBe(3); atom.config.set('a.c', 5); expect(atom.config.get('a.c')).toBe(5); atom.config.unset('a.c'); expect(atom.config.get('a.c')).toBeUndefined(); }); it('calls ::save()', () => { atom.config.setDefaults('a', { b: 3 }); atom.config.set('a.b', 4); savedSettings.length = 0; atom.config.unset('a.c'); advanceClock(500); expect(savedSettings.length).toBe(1); }); describe("when no 'scopeSelector' is given", () => { describe("when a 'source' but no key-path is given", () => it('removes all scoped settings with the given source', () => { atom.config.set('foo.bar.baz', 1, { scopeSelector: '.a', source: 'source-a' }); atom.config.set('foo.bar.quux', 2, { scopeSelector: '.b', source: 'source-a' }); expect(atom.config.get('foo.bar', { scope: ['.a.b'] })).toEqual({ baz: 1, quux: 2 }); atom.config.unset(null, { source: 'source-a' }); expect(atom.config.get('foo.bar', { scope: ['.a'] })).toEqual({ baz: 0, ok: 0 }); })); describe("when a 'source' and a key-path is given", () => it('removes all scoped settings with the given source and key-path', () => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2, { scopeSelector: '.a', source: 'source-a' }); atom.config.set('foo.bar.baz', 3, { scopeSelector: '.a.b', source: 'source-b' }); expect(atom.config.get('foo.bar.baz', { scope: ['.a.b'] })).toEqual( 3 ); atom.config.unset('foo.bar.baz', { source: 'source-b' }); expect(atom.config.get('foo.bar.baz', { scope: ['.a.b'] })).toEqual( 2 ); expect(atom.config.get('foo.bar.baz')).toEqual(1); })); describe("when no 'source' is given", () => it('removes all scoped and unscoped properties for that key-path', () => { atom.config.setDefaults('foo.bar', { baz: 100 }); atom.config.set( 'foo.bar', { baz: 1, ok: 2 }, { scopeSelector: '.a' } ); atom.config.set( 'foo.bar', { baz: 11, ok: 12 }, { scopeSelector: '.b' } ); atom.config.set('foo.bar', { baz: 21, ok: 22 }); atom.config.unset('foo.bar.baz'); expect(atom.config.get('foo.bar.baz', { scope: ['.a'] })).toBe(100); expect(atom.config.get('foo.bar.baz', { scope: ['.b'] })).toBe(100); expect(atom.config.get('foo.bar.baz')).toBe(100); expect(atom.config.get('foo.bar.ok', { scope: ['.a'] })).toBe(2); expect(atom.config.get('foo.bar.ok', { scope: ['.b'] })).toBe(12); expect(atom.config.get('foo.bar.ok')).toBe(22); })); }); describe("when a 'scopeSelector' is given", () => { it('restores the global default when no scoped default set', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(55); atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(10); }); it('restores the scoped default when a scoped default is set', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee', source: 'some-source' }); atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee' }); atom.config.set('foo.bar.ok', 100, { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(55); atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(42); expect( atom.config.get('foo.bar.ok', { scope: ['.source.coffee'] }) ).toBe(100); }); it('calls ::save()', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee' }); savedSettings.length = 0; atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); advanceClock(150); expect(savedSettings.length).toBe(1); }); it('allows removing settings for a specific source and scope selector', () => { atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee', source: 'source-a' }); atom.config.set('foo.bar.baz', 65, { scopeSelector: '.source.coffee', source: 'source-b' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(65); atom.config.unset('foo.bar.baz', { source: 'source-b', scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string'] }) ).toBe(55); }); it('allows removing all settings for a specific source', () => { atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee', source: 'source-a' }); atom.config.set('foo.bar.baz', 65, { scopeSelector: '.source.coffee', source: 'source-b' }); atom.config.set('foo.bar.ok', 65, { scopeSelector: '.source.coffee', source: 'source-b' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(65); atom.config.unset(null, { source: 'source-b', scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string'] }) ).toBe(55); expect( atom.config.get('foo.bar.ok', { scope: ['.source.coffee', '.string'] }) ).toBe(0); }); it('does not call ::save or add a scoped property when no value has been set', () => { // see https://github.com/atom/atom/issues/4175 atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(10); expect(savedSettings.length).toBe(0); const scopedProperties = atom.config.scopedSettingsStore.propertiesForSource( 'user-config' ); expect(scopedProperties['.coffee.source']).toBeUndefined(); }); it('removes the scoped value when it was the only set value on the object', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee' }); atom.config.set('foo.bar.ok', 20, { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(55); advanceClock(150); savedSettings.length = 0; atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(10); expect( atom.config.get('foo.bar.ok', { scope: ['.source.coffee'] }) ).toBe(20); advanceClock(150); expect(savedSettings[0]['.coffee.source']).toEqual({ foo: { bar: { ok: 20 } } }); atom.config.unset('foo.bar.ok', { scopeSelector: '.source.coffee' }); advanceClock(150); expect(savedSettings.length).toBe(2); expect(savedSettings[1]['.coffee.source']).toBeUndefined(); }); it('does not call ::save when the value is already at the default', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 55); advanceClock(150); savedSettings.length = 0; atom.config.unset('foo.bar.ok', { scopeSelector: '.source.coffee' }); advanceClock(150); expect(savedSettings.length).toBe(0); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(55); }); }); }); describe('.onDidChange(keyPath, {scope})', () => { let observeHandler = []; describe('when a keyPath is specified', () => { beforeEach(() => { observeHandler = jasmine.createSpy('observeHandler'); atom.config.set('foo.bar.baz', 'value 1'); atom.config.onDidChange('foo.bar.baz', observeHandler); }); it('does not fire the given callback with the current value at the keypath', () => expect(observeHandler).not.toHaveBeenCalled()); it('fires the callback every time the observed value changes', () => { atom.config.set('foo.bar.baz', 'value 2'); expect(observeHandler).toHaveBeenCalledWith({ newValue: 'value 2', oldValue: 'value 1' }); observeHandler.reset(); observeHandler.andCallFake(() => { throw new Error('oops'); }); expect(() => atom.config.set('foo.bar.baz', 'value 1')).toThrow('oops'); expect(observeHandler).toHaveBeenCalledWith({ newValue: 'value 1', oldValue: 'value 2' }); observeHandler.reset(); // Regression: exception in earlier handler shouldn't put observer // into a bad state. atom.config.set('something.else', 'new value'); expect(observeHandler).not.toHaveBeenCalled(); }); }); describe('when a keyPath is not specified', () => { beforeEach(() => { observeHandler = jasmine.createSpy('observeHandler'); atom.config.set('foo.bar.baz', 'value 1'); atom.config.onDidChange(observeHandler); }); it('does not fire the given callback initially', () => expect(observeHandler).not.toHaveBeenCalled()); it('fires the callback every time any value changes', () => { observeHandler.reset(); // clear the initial call atom.config.set('foo.bar.baz', 'value 2'); expect(observeHandler).toHaveBeenCalled(); expect(observeHandler.mostRecentCall.args[0].newValue.foo.bar.baz).toBe( 'value 2' ); expect(observeHandler.mostRecentCall.args[0].oldValue.foo.bar.baz).toBe( 'value 1' ); observeHandler.reset(); atom.config.set('foo.bar.baz', 'value 1'); expect(observeHandler).toHaveBeenCalled(); expect(observeHandler.mostRecentCall.args[0].newValue.foo.bar.baz).toBe( 'value 1' ); expect(observeHandler.mostRecentCall.args[0].oldValue.foo.bar.baz).toBe( 'value 2' ); observeHandler.reset(); atom.config.set('foo.bar.int', 1); expect(observeHandler).toHaveBeenCalled(); expect(observeHandler.mostRecentCall.args[0].newValue.foo.bar.int).toBe( 1 ); expect(observeHandler.mostRecentCall.args[0].oldValue.foo.bar.int).toBe( undefined ); }); }); describe("when a 'scope' is given", () => it('calls the supplied callback when the value at the descriptor/keypath changes', () => { const changeSpy = jasmine.createSpy('onDidChange callback'); atom.config.onDidChange( 'foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }, changeSpy ); atom.config.set('foo.bar.baz', 12); expect(changeSpy).toHaveBeenCalledWith({ oldValue: undefined, newValue: 12 }); changeSpy.reset(); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source .string.quoted.double', source: 'a' }); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 12, newValue: 22 }); changeSpy.reset(); atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.double.coffee', source: 'b' }); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 22, newValue: 42 }); changeSpy.reset(); atom.config.unset(null, { scopeSelector: '.source.coffee .string.quoted.double.coffee', source: 'b' }); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 42, newValue: 22 }); changeSpy.reset(); atom.config.unset(null, { scopeSelector: '.source .string.quoted.double', source: 'a' }); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 22, newValue: 12 }); changeSpy.reset(); atom.config.set('foo.bar.baz', undefined); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 12, newValue: undefined }); changeSpy.reset(); })); }); describe('.observe(keyPath, {scope})', () => { let [observeHandler, observeSubscription] = []; beforeEach(() => { observeHandler = jasmine.createSpy('observeHandler'); atom.config.set('foo.bar.baz', 'value 1'); observeSubscription = atom.config.observe('foo.bar.baz', observeHandler); }); it('fires the given callback with the current value at the keypath', () => expect(observeHandler).toHaveBeenCalledWith('value 1')); it('fires the callback every time the observed value changes', () => { observeHandler.reset(); // clear the initial call atom.config.set('foo.bar.baz', 'value 2'); expect(observeHandler).toHaveBeenCalledWith('value 2'); observeHandler.reset(); atom.config.set('foo.bar.baz', 'value 1'); expect(observeHandler).toHaveBeenCalledWith('value 1'); advanceClock(100); // complete pending save that was requested in ::set observeHandler.reset(); atom.config.resetUserSettings({ foo: {} }); expect(observeHandler).toHaveBeenCalledWith(undefined); }); it('fires the callback when the observed value is deleted', () => { observeHandler.reset(); // clear the initial call atom.config.set('foo.bar.baz', undefined); expect(observeHandler).toHaveBeenCalledWith(undefined); }); it('fires the callback when the full key path goes into and out of existence', () => { observeHandler.reset(); // clear the initial call atom.config.set('foo.bar', undefined); expect(observeHandler).toHaveBeenCalledWith(undefined); observeHandler.reset(); atom.config.set('foo.bar.baz', "i'm back"); expect(observeHandler).toHaveBeenCalledWith("i'm back"); }); it('does not fire the callback once the subscription is disposed', () => { observeHandler.reset(); // clear the initial call observeSubscription.dispose(); atom.config.set('foo.bar.baz', 'value 2'); expect(observeHandler).not.toHaveBeenCalled(); }); it('does not fire the callback for a similarly named keyPath', () => { const bazCatHandler = jasmine.createSpy('bazCatHandler'); observeSubscription = atom.config.observe( 'foo.bar.bazCat', bazCatHandler ); bazCatHandler.reset(); atom.config.set('foo.bar.baz', 'value 10'); expect(bazCatHandler).not.toHaveBeenCalled(); }); describe("when a 'scope' is given", () => { let otherHandler = null; beforeEach(() => { observeSubscription.dispose(); otherHandler = jasmine.createSpy('otherHandler'); }); it('allows settings to be observed in a specific scope', () => { atom.config.observe( 'foo.bar.baz', { scope: ['.some.scope'] }, observeHandler ); atom.config.observe( 'foo.bar.baz', { scope: ['.another.scope'] }, otherHandler ); atom.config.set('foo.bar.baz', 'value 2', { scopeSelector: '.some' }); expect(observeHandler).toHaveBeenCalledWith('value 2'); expect(otherHandler).not.toHaveBeenCalledWith('value 2'); }); it('calls the callback when properties with more specific selectors are removed', () => { const changeSpy = jasmine.createSpy(); atom.config.observe( 'foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }, changeSpy ); expect(changeSpy).toHaveBeenCalledWith('value 1'); changeSpy.reset(); atom.config.set('foo.bar.baz', 12); expect(changeSpy).toHaveBeenCalledWith(12); changeSpy.reset(); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source .string.quoted.double', source: 'a' }); expect(changeSpy).toHaveBeenCalledWith(22); changeSpy.reset(); atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.double.coffee', source: 'b' }); expect(changeSpy).toHaveBeenCalledWith(42); changeSpy.reset(); atom.config.unset(null, { scopeSelector: '.source.coffee .string.quoted.double.coffee', source: 'b' }); expect(changeSpy).toHaveBeenCalledWith(22); changeSpy.reset(); atom.config.unset(null, { scopeSelector: '.source .string.quoted.double', source: 'a' }); expect(changeSpy).toHaveBeenCalledWith(12); changeSpy.reset(); atom.config.set('foo.bar.baz', undefined); expect(changeSpy).toHaveBeenCalledWith(undefined); changeSpy.reset(); }); }); }); describe('.transact(callback)', () => { let changeSpy = null; beforeEach(() => { changeSpy = jasmine.createSpy('onDidChange callback'); atom.config.onDidChange('foo.bar.baz', changeSpy); }); it('allows only one change event for the duration of the given callback', () => { atom.config.transact(() => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2); atom.config.set('foo.bar.baz', 3); }); expect(changeSpy.callCount).toBe(1); expect(changeSpy.argsForCall[0][0]).toEqual({ newValue: 3, oldValue: undefined }); }); it('does not emit an event if no changes occur while paused', () => { atom.config.transact(() => {}); expect(changeSpy).not.toHaveBeenCalled(); }); }); describe('.transactAsync(callback)', () => { let changeSpy = null; beforeEach(() => { changeSpy = jasmine.createSpy('onDidChange callback'); atom.config.onDidChange('foo.bar.baz', changeSpy); }); it('allows only one change event for the duration of the given promise if it gets resolved', () => { let promiseResult = null; const transactionPromise = atom.config.transactAsync(() => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2); atom.config.set('foo.bar.baz', 3); return Promise.resolve('a result'); }); waitsForPromise(() => transactionPromise.then(result => { promiseResult = result; }) ); runs(() => { expect(promiseResult).toBe('a result'); expect(changeSpy.callCount).toBe(1); expect(changeSpy.argsForCall[0][0]).toEqual({ newValue: 3, oldValue: undefined }); }); }); it('allows only one change event for the duration of the given promise if it gets rejected', () => { let promiseError = null; const transactionPromise = atom.config.transactAsync(() => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2); atom.config.set('foo.bar.baz', 3); return Promise.reject(new Error('an error')); }); waitsForPromise(() => transactionPromise.catch(error => { promiseError = error; }) ); runs(() => { expect(promiseError.message).toBe('an error'); expect(changeSpy.callCount).toBe(1); expect(changeSpy.argsForCall[0][0]).toEqual({ newValue: 3, oldValue: undefined }); }); }); it('allows only one change event even when the given callback throws', () => { const error = new Error('Oops!'); let promiseError = null; const transactionPromise = atom.config.transactAsync(() => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2); atom.config.set('foo.bar.baz', 3); throw error; }); waitsForPromise(() => transactionPromise.catch(e => { promiseError = e; }) ); runs(() => { expect(promiseError).toBe(error); expect(changeSpy.callCount).toBe(1); expect(changeSpy.argsForCall[0][0]).toEqual({ newValue: 3, oldValue: undefined }); }); }); }); describe('.getSources()', () => { it("returns an array of all of the config's source names", () => { expect(atom.config.getSources()).toEqual([]); atom.config.set('a.b', 1, { scopeSelector: '.x1', source: 'source-1' }); atom.config.set('a.c', 1, { scopeSelector: '.x1', source: 'source-1' }); atom.config.set('a.b', 2, { scopeSelector: '.x2', source: 'source-2' }); atom.config.set('a.b', 1, { scopeSelector: '.x3', source: 'source-3' }); expect(atom.config.getSources()).toEqual([ 'source-1', 'source-2', 'source-3' ]); }); }); describe('.save()', () => { it('calls the save callback with any non-default properties', () => { atom.config.set('a.b.c', 1); atom.config.set('a.b.d', 2); atom.config.set('x.y.z', 3); atom.config.setDefaults('a.b', { e: 4, f: 5 }); atom.config.save(); expect(savedSettings).toEqual([{ '*': atom.config.settings }]); }); it('serializes properties in alphabetical order', () => { atom.config.set('foo', 1); atom.config.set('bar', 2); atom.config.set('baz.foo', 3); atom.config.set('baz.bar', 4); savedSettings.length = 0; atom.config.save(); const writtenConfig = savedSettings[0]; expect(writtenConfig).toEqual({ '*': atom.config.settings }); let expectedKeys = ['bar', 'baz', 'foo']; let foundKeys = []; for (const key in writtenConfig['*']) { if (expectedKeys.includes(key)) { foundKeys.push(key); } } expect(foundKeys).toEqual(expectedKeys); expectedKeys = ['bar', 'foo']; foundKeys = []; for (const key in writtenConfig['*']['baz']) { if (expectedKeys.includes(key)) { foundKeys.push(key); } } expect(foundKeys).toEqual(expectedKeys); }); describe('when scoped settings are defined', () => { it('serializes any explicitly set config settings', () => { atom.config.set('foo.bar', 'ruby', { scopeSelector: '.source.ruby' }); atom.config.set('foo.omg', 'wow', { scopeSelector: '.source.ruby' }); atom.config.set('foo.bar', 'coffee', { scopeSelector: '.source.coffee' }); savedSettings.length = 0; atom.config.save(); const writtenConfig = savedSettings[0]; expect(writtenConfig).toEqualJson({ '*': atom.config.settings, '.ruby.source': { foo: { bar: 'ruby', omg: 'wow' } }, '.coffee.source': { foo: { bar: 'coffee' } } }); }); }); }); describe('.resetUserSettings()', () => { beforeEach(() => { atom.config.setSchema('foo', { type: 'object', properties: { bar: { type: 'string', default: 'def' }, int: { type: 'integer', default: 12 } } }); }); describe('when the config file contains scoped settings', () => { it('updates the config data based on the file contents', () => { atom.config.resetUserSettings({ '*': { foo: { bar: 'baz' } }, '.source.ruby': { foo: { bar: 'more-specific' } } }); expect(atom.config.get('foo.bar')).toBe('baz'); expect(atom.config.get('foo.bar', { scope: ['.source.ruby'] })).toBe( 'more-specific' ); }); }); describe('when the config file does not conform to the schema', () => { it('validates and does not load the incorrect values', () => { atom.config.resetUserSettings({ '*': { foo: { bar: 'omg', int: 'baz' } }, '.source.ruby': { foo: { bar: 'scoped', int: 'nope' } } }); expect(atom.config.get('foo.int')).toBe(12); expect(atom.config.get('foo.bar')).toBe('omg'); expect(atom.config.get('foo.int', { scope: ['.source.ruby'] })).toBe( 12 ); expect(atom.config.get('foo.bar', { scope: ['.source.ruby'] })).toBe( 'scoped' ); }); }); it('updates the config data based on the file contents', () => { atom.config.resetUserSettings({ foo: { bar: 'baz' } }); expect(atom.config.get('foo.bar')).toBe('baz'); }); it('notifies observers for updated keypaths on load', () => { const observeHandler = jasmine.createSpy('observeHandler'); atom.config.observe('foo.bar', observeHandler); atom.config.resetUserSettings({ foo: { bar: 'baz' } }); expect(observeHandler).toHaveBeenCalledWith('baz'); }); describe('when the config file contains values that do not adhere to the schema', () => { it('updates the only the settings that have values matching the schema', () => { atom.config.resetUserSettings({ foo: { bar: 'baz', int: 'bad value' } }); expect(atom.config.get('foo.bar')).toBe('baz'); expect(atom.config.get('foo.int')).toBe(12); expect(console.warn).toHaveBeenCalled(); expect(console.warn.mostRecentCall.args[0]).toContain('foo.int'); }); }); it('does not fire a change event for paths that did not change', () => { atom.config.resetUserSettings({ foo: { bar: 'baz', int: 3 } }); const noChangeSpy = jasmine.createSpy('unchanged'); atom.config.onDidChange('foo.bar', noChangeSpy); atom.config.resetUserSettings({ foo: { bar: 'baz', int: 4 } }); expect(noChangeSpy).not.toHaveBeenCalled(); expect(atom.config.get('foo.bar')).toBe('baz'); expect(atom.config.get('foo.int')).toBe(4); }); it('does not fire a change event for paths whose non-primitive values did not change', () => { atom.config.setSchema('foo.bar', { type: 'array', items: { type: 'string' } }); atom.config.resetUserSettings({ foo: { bar: ['baz', 'quux'], int: 2 } }); const noChangeSpy = jasmine.createSpy('unchanged'); atom.config.onDidChange('foo.bar', noChangeSpy); atom.config.resetUserSettings({ foo: { bar: ['baz', 'quux'], int: 2 } }); expect(noChangeSpy).not.toHaveBeenCalled(); expect(atom.config.get('foo.bar')).toEqual(['baz', 'quux']); }); describe('when a setting with a default is removed', () => { it('resets the setting back to the default', () => { atom.config.resetUserSettings({ foo: { bar: ['baz', 'quux'], int: 2 } }); const events = []; atom.config.onDidChange('foo.int', event => events.push(event)); atom.config.resetUserSettings({ foo: { bar: ['baz', 'quux'] } }); expect(events.length).toBe(1); expect(events[0]).toEqual({ oldValue: 2, newValue: 12 }); }); }); it('keeps all the global scope settings after overriding one', () => { atom.config.resetUserSettings({ '*': { foo: { bar: 'baz', int: 99 } } }); atom.config.set('foo.int', 50, { scopeSelector: '*' }); advanceClock(100); expect(savedSettings[0]['*'].foo).toEqual({ bar: 'baz', int: 50 }); expect(atom.config.get('foo.int', { scope: ['*'] })).toEqual(50); expect(atom.config.get('foo.bar', { scope: ['*'] })).toEqual('baz'); expect(atom.config.get('foo.int')).toEqual(50); }); }); describe('.pushAtKeyPath(keyPath, value)', () => { it('pushes the given value to the array at the key path and updates observers', () => { atom.config.set('foo.bar.baz', ['a']); const observeHandler = jasmine.createSpy('observeHandler'); atom.config.observe('foo.bar.baz', observeHandler); observeHandler.reset(); expect(atom.config.pushAtKeyPath('foo.bar.baz', 'b')).toBe(2); expect(atom.config.get('foo.bar.baz')).toEqual(['a', 'b']); expect(observeHandler).toHaveBeenCalledWith( atom.config.get('foo.bar.baz') ); }); }); describe('.unshiftAtKeyPath(keyPath, value)', () => { it('unshifts the given value to the array at the key path and updates observers', () => { atom.config.set('foo.bar.baz', ['b']); const observeHandler = jasmine.createSpy('observeHandler'); atom.config.observe('foo.bar.baz', observeHandler); observeHandler.reset(); expect(atom.config.unshiftAtKeyPath('foo.bar.baz', 'a')).toBe(2); expect(atom.config.get('foo.bar.baz')).toEqual(['a', 'b']); expect(observeHandler).toHaveBeenCalledWith( atom.config.get('foo.bar.baz') ); }); }); describe('.removeAtKeyPath(keyPath, value)', () => { it('removes the given value from the array at the key path and updates observers', () => { atom.config.set('foo.bar.baz', ['a', 'b', 'c']); const observeHandler = jasmine.createSpy('observeHandler'); atom.config.observe('foo.bar.baz', observeHandler); observeHandler.reset(); expect(atom.config.removeAtKeyPath('foo.bar.baz', 'b')).toEqual([ 'a', 'c' ]); expect(atom.config.get('foo.bar.baz')).toEqual(['a', 'c']); expect(observeHandler).toHaveBeenCalledWith( atom.config.get('foo.bar.baz') ); }); }); describe('.setDefaults(keyPath, defaults)', () => { it('assigns any previously-unassigned keys to the object at the key path', () => { atom.config.set('foo.bar.baz', { a: 1 }); atom.config.setDefaults('foo.bar.baz', { a: 2, b: 3, c: 4 }); expect(atom.config.get('foo.bar.baz.a')).toBe(1); expect(atom.config.get('foo.bar.baz.b')).toBe(3); expect(atom.config.get('foo.bar.baz.c')).toBe(4); atom.config.setDefaults('foo.quux', { x: 0, y: 1 }); expect(atom.config.get('foo.quux.x')).toBe(0); expect(atom.config.get('foo.quux.y')).toBe(1); }); it('emits an updated event', () => { const updatedCallback = jasmine.createSpy('updated'); atom.config.onDidChange('foo.bar.baz.a', updatedCallback); expect(updatedCallback.callCount).toBe(0); atom.config.setDefaults('foo.bar.baz', { a: 2 }); expect(updatedCallback.callCount).toBe(1); }); }); describe('.setSchema(keyPath, schema)', () => { it('creates a properly nested schema', () => { const schema = { type: 'object', properties: { anInt: { type: 'integer', default: 12 } } }; atom.config.setSchema('foo.bar', schema); expect(atom.config.getSchema('foo')).toEqual({ type: 'object', properties: { bar: { type: 'object', properties: { anInt: { type: 'integer', default: 12 } } } } }); }); it('sets defaults specified by the schema', () => { const schema = { type: 'object', properties: { anInt: { type: 'integer', default: 12 }, anObject: { type: 'object', properties: { nestedInt: { type: 'integer', default: 24 }, nestedObject: { type: 'object', properties: { superNestedInt: { type: 'integer', default: 36 } } } } } } }; atom.config.setSchema('foo.bar', schema); expect(atom.config.get('foo.bar.anInt')).toBe(12); expect(atom.config.get('foo.bar.anObject')).toEqual({ nestedInt: 24, nestedObject: { superNestedInt: 36 } }); expect(atom.config.get('foo')).toEqual({ bar: { anInt: 12, anObject: { nestedInt: 24, nestedObject: { superNestedInt: 36 } } } }); atom.config.set('foo.bar.anObject.nestedObject.superNestedInt', 37); expect(atom.config.get('foo')).toEqual({ bar: { anInt: 12, anObject: { nestedInt: 24, nestedObject: { superNestedInt: 37 } } } }); }); it('can set a non-object schema', () => { const schema = { type: 'integer', default: 12 }; atom.config.setSchema('foo.bar.anInt', schema); expect(atom.config.get('foo.bar.anInt')).toBe(12); expect(atom.config.getSchema('foo.bar.anInt')).toEqual({ type: 'integer', default: 12 }); }); it('allows the schema to be retrieved via ::getSchema', () => { const schema = { type: 'object', properties: { anInt: { type: 'integer', default: 12 } } }; atom.config.setSchema('foo.bar', schema); expect(atom.config.getSchema('foo.bar')).toEqual({ type: 'object', properties: { anInt: { type: 'integer', default: 12 } } }); expect(atom.config.getSchema('foo.bar.anInt')).toEqual({ type: 'integer', default: 12 }); expect(atom.config.getSchema('foo.baz')).toEqual({ type: 'any' }); expect(atom.config.getSchema('foo.bar.anInt.baz')).toBe(null); }); it('respects the schema for scoped settings', () => { const schema = { type: 'string', default: 'ok', scopes: { '.source.js': { default: 'omg' } } }; atom.config.setSchema('foo.bar.str', schema); expect(atom.config.get('foo.bar.str')).toBe('ok'); expect(atom.config.get('foo.bar.str', { scope: ['.source.js'] })).toBe( 'omg' ); expect( atom.config.get('foo.bar.str', { scope: ['.source.coffee'] }) ).toBe('ok'); }); describe('when a schema is added after config values have been set', () => { let schema = null; beforeEach(() => { schema = { type: 'object', properties: { int: { type: 'integer', default: 2 }, str: { type: 'string', default: 'def' } } }; }); it('respects the new schema when values are set', () => { expect(atom.config.set('foo.bar.str', 'global')).toBe(true); expect( atom.config.set('foo.bar.str', 'scoped', { scopeSelector: '.source.js' }) ).toBe(true); expect(atom.config.get('foo.bar.str')).toBe('global'); expect(atom.config.get('foo.bar.str', { scope: ['.source.js'] })).toBe( 'scoped' ); expect(atom.config.set('foo.bar.noschema', 'nsGlobal')).toBe(true); expect( atom.config.set('foo.bar.noschema', 'nsScoped', { scopeSelector: '.source.js' }) ).toBe(true); expect(atom.config.get('foo.bar.noschema')).toBe('nsGlobal'); expect( atom.config.get('foo.bar.noschema', { scope: ['.source.js'] }) ).toBe('nsScoped'); expect(atom.config.set('foo.bar.int', 'nope')).toBe(true); expect( atom.config.set('foo.bar.int', 'notanint', { scopeSelector: '.source.js' }) ).toBe(true); expect( atom.config.set('foo.bar.int', 23, { scopeSelector: '.source.coffee' }) ).toBe(true); expect(atom.config.get('foo.bar.int')).toBe('nope'); expect(atom.config.get('foo.bar.int', { scope: ['.source.js'] })).toBe( 'notanint' ); expect( atom.config.get('foo.bar.int', { scope: ['.source.coffee'] }) ).toBe(23); atom.config.setSchema('foo.bar', schema); expect(atom.config.get('foo.bar.str')).toBe('global'); expect(atom.config.get('foo.bar.str', { scope: ['.source.js'] })).toBe( 'scoped' ); expect(atom.config.get('foo.bar.noschema')).toBe('nsGlobal'); expect( atom.config.get('foo.bar.noschema', { scope: ['.source.js'] }) ).toBe('nsScoped'); expect(atom.config.get('foo.bar.int')).toBe(2); expect(atom.config.get('foo.bar.int', { scope: ['.source.js'] })).toBe( 2 ); expect( atom.config.get('foo.bar.int', { scope: ['.source.coffee'] }) ).toBe(23); }); it('sets all values that adhere to the schema', () => { expect(atom.config.set('foo.bar.int', 10)).toBe(true); expect( atom.config.set('foo.bar.int', 15, { scopeSelector: '.source.js' }) ).toBe(true); expect( atom.config.set('foo.bar.int', 23, { scopeSelector: '.source.coffee' }) ).toBe(true); expect(atom.config.get('foo.bar.int')).toBe(10); expect(atom.config.get('foo.bar.int', { scope: ['.source.js'] })).toBe( 15 ); expect( atom.config.get('foo.bar.int', { scope: ['.source.coffee'] }) ).toBe(23); atom.config.setSchema('foo.bar', schema); expect(atom.config.get('foo.bar.int')).toBe(10); expect(atom.config.get('foo.bar.int', { scope: ['.source.js'] })).toBe( 15 ); expect( atom.config.get('foo.bar.int', { scope: ['.source.coffee'] }) ).toBe(23); }); }); describe('when the value has an "integer" type', () => { beforeEach(() => { const schema = { type: 'integer', default: 12 }; atom.config.setSchema('foo.bar.anInt', schema); }); it('coerces a string to an int', () => { atom.config.set('foo.bar.anInt', '123'); expect(atom.config.get('foo.bar.anInt')).toBe(123); }); it('does not allow infinity', () => { atom.config.set('foo.bar.anInt', Infinity); expect(atom.config.get('foo.bar.anInt')).toBe(12); }); it('coerces a float to an int', () => { atom.config.set('foo.bar.anInt', 12.3); expect(atom.config.get('foo.bar.anInt')).toBe(12); }); it('will not set non-integers', () => { atom.config.set('foo.bar.anInt', null); expect(atom.config.get('foo.bar.anInt')).toBe(12); atom.config.set('foo.bar.anInt', 'nope'); expect(atom.config.get('foo.bar.anInt')).toBe(12); }); describe('when the minimum and maximum keys are used', () => { beforeEach(() => { const schema = { type: 'integer', minimum: 10, maximum: 20, default: 12 }; atom.config.setSchema('foo.bar.anInt', schema); }); it('keeps the specified value within the specified range', () => { atom.config.set('foo.bar.anInt', '123'); expect(atom.config.get('foo.bar.anInt')).toBe(20); atom.config.set('foo.bar.anInt', '1'); expect(atom.config.get('foo.bar.anInt')).toBe(10); }); }); }); describe('when the value has an "integer" and "string" type', () => { beforeEach(() => { const schema = { type: ['integer', 'string'], default: 12 }; atom.config.setSchema('foo.bar.anInt', schema); }); it('can coerce an int, and fallback to a string', () => { atom.config.set('foo.bar.anInt', '123'); expect(atom.config.get('foo.bar.anInt')).toBe(123); atom.config.set('foo.bar.anInt', 'cats'); expect(atom.config.get('foo.bar.anInt')).toBe('cats'); }); }); describe('when the value has an "string" and "boolean" type', () => { beforeEach(() => { const schema = { type: ['string', 'boolean'], default: 'def' }; atom.config.setSchema('foo.bar', schema); }); it('can set a string, a boolean, and revert back to the default', () => { atom.config.set('foo.bar', 'ok'); expect(atom.config.get('foo.bar')).toBe('ok'); atom.config.set('foo.bar', false); expect(atom.config.get('foo.bar')).toBe(false); atom.config.set('foo.bar', undefined); expect(atom.config.get('foo.bar')).toBe('def'); }); }); describe('when the value has a "number" type', () => { beforeEach(() => { const schema = { type: 'number', default: 12.1 }; atom.config.setSchema('foo.bar.aFloat', schema); }); it('coerces a string to a float', () => { atom.config.set('foo.bar.aFloat', '12.23'); expect(atom.config.get('foo.bar.aFloat')).toBe(12.23); }); it('will not set non-numbers', () => { atom.config.set('foo.bar.aFloat', null); expect(atom.config.get('foo.bar.aFloat')).toBe(12.1); atom.config.set('foo.bar.aFloat', 'nope'); expect(atom.config.get('foo.bar.aFloat')).toBe(12.1); }); describe('when the minimum and maximum keys are used', () => { beforeEach(() => { const schema = { type: 'number', minimum: 11.2, maximum: 25.4, default: 12.1 }; atom.config.setSchema('foo.bar.aFloat', schema); }); it('keeps the specified value within the specified range', () => { atom.config.set('foo.bar.aFloat', '123.2'); expect(atom.config.get('foo.bar.aFloat')).toBe(25.4); atom.config.set('foo.bar.aFloat', '1.0'); expect(atom.config.get('foo.bar.aFloat')).toBe(11.2); }); }); }); describe('when the value has a "boolean" type', () => { beforeEach(() => { const schema = { type: 'boolean', default: true }; atom.config.setSchema('foo.bar.aBool', schema); }); it('coerces various types to a boolean', () => { atom.config.set('foo.bar.aBool', 'true'); expect(atom.config.get('foo.bar.aBool')).toBe(true); atom.config.set('foo.bar.aBool', 'false'); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', 'TRUE'); expect(atom.config.get('foo.bar.aBool')).toBe(true); atom.config.set('foo.bar.aBool', 'FALSE'); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', 1); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', 0); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', {}); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', null); expect(atom.config.get('foo.bar.aBool')).toBe(false); }); it('reverts back to the default value when undefined is passed to set', () => { atom.config.set('foo.bar.aBool', 'false'); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', undefined); expect(atom.config.get('foo.bar.aBool')).toBe(true); }); }); describe('when the value has an "string" type', () => { beforeEach(() => { const schema = { type: 'string', default: 'ok' }; atom.config.setSchema('foo.bar.aString', schema); }); it('allows strings', () => { atom.config.set('foo.bar.aString', 'yep'); expect(atom.config.get('foo.bar.aString')).toBe('yep'); }); it('will only set strings', () => { expect(atom.config.set('foo.bar.aString', 123)).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); expect(atom.config.set('foo.bar.aString', true)).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); expect(atom.config.set('foo.bar.aString', null)).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); expect(atom.config.set('foo.bar.aString', [])).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); expect(atom.config.set('foo.bar.aString', { nope: 'nope' })).toBe( false ); expect(atom.config.get('foo.bar.aString')).toBe('ok'); }); it('does not allow setting children of that key-path', () => { expect(atom.config.set('foo.bar.aString.something', 123)).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); }); describe('when the schema has a "maximumLength" key', () => it('trims the string to be no longer than the specified maximum', () => { const schema = { type: 'string', default: 'ok', maximumLength: 3 }; atom.config.setSchema('foo.bar.aString', schema); atom.config.set('foo.bar.aString', 'abcdefg'); expect(atom.config.get('foo.bar.aString')).toBe('abc'); })); }); describe('when the value has an "object" type', () => { beforeEach(() => { const schema = { type: 'object', properties: { anInt: { type: 'integer', default: 12 }, nestedObject: { type: 'object', properties: { nestedBool: { type: 'boolean', default: false } } } } }; atom.config.setSchema('foo.bar', schema); }); it('converts and validates all the children', () => { atom.config.set('foo.bar', { anInt: '23', nestedObject: { nestedBool: 'true' } }); expect(atom.config.get('foo.bar')).toEqual({ anInt: 23, nestedObject: { nestedBool: true } }); }); it('will set only the values that adhere to the schema', () => { expect( atom.config.set('foo.bar', { anInt: 'nope', nestedObject: { nestedBool: true } }) ).toBe(true); expect(atom.config.get('foo.bar.anInt')).toEqual(12); expect(atom.config.get('foo.bar.nestedObject.nestedBool')).toEqual( true ); }); describe('when the value has additionalProperties set to false', () => it('does not allow other properties to be set on the object', () => { atom.config.setSchema('foo.bar', { type: 'object', properties: { anInt: { type: 'integer', default: 12 } }, additionalProperties: false }); expect( atom.config.set('foo.bar', { anInt: 5, somethingElse: 'ok' }) ).toBe(true); expect(atom.config.get('foo.bar.anInt')).toBe(5); expect(atom.config.get('foo.bar.somethingElse')).toBeUndefined(); expect(atom.config.set('foo.bar.somethingElse', { anInt: 5 })).toBe( false ); expect(atom.config.get('foo.bar.somethingElse')).toBeUndefined(); })); describe('when the value has an additionalProperties schema', () => it('validates properties of the object against that schema', () => { atom.config.setSchema('foo.bar', { type: 'object', properties: { anInt: { type: 'integer', default: 12 } }, additionalProperties: { type: 'string' } }); expect( atom.config.set('foo.bar', { anInt: 5, somethingElse: 'ok' }) ).toBe(true); expect(atom.config.get('foo.bar.anInt')).toBe(5); expect(atom.config.get('foo.bar.somethingElse')).toBe('ok'); expect(atom.config.set('foo.bar.somethingElse', 7)).toBe(false); expect(atom.config.get('foo.bar.somethingElse')).toBe('ok'); expect( atom.config.set('foo.bar', { anInt: 6, somethingElse: 7 }) ).toBe(true); expect(atom.config.get('foo.bar.anInt')).toBe(6); expect(atom.config.get('foo.bar.somethingElse')).toBe(undefined); })); }); describe('when the value has an "array" type', () => { beforeEach(() => { const schema = { type: 'array', default: [1, 2, 3], items: { type: 'integer' } }; atom.config.setSchema('foo.bar', schema); }); it('converts an array of strings to an array of ints', () => { atom.config.set('foo.bar', ['2', '3', '4']); expect(atom.config.get('foo.bar')).toEqual([2, 3, 4]); }); it('does not allow setting children of that key-path', () => { expect(atom.config.set('foo.bar.child', 123)).toBe(false); expect(atom.config.set('foo.bar.child.grandchild', 123)).toBe(false); expect(atom.config.get('foo.bar')).toEqual([1, 2, 3]); }); }); describe('when the value has a "color" type', () => { beforeEach(() => { const schema = { type: 'color', default: 'white' }; atom.config.setSchema('foo.bar.aColor', schema); }); it('returns a Color object', () => { let color = atom.config.get('foo.bar.aColor'); expect(color.toHexString()).toBe('#ffffff'); expect(color.toRGBAString()).toBe('rgba(255, 255, 255, 1)'); color.red = 0; color.green = 0; color.blue = 0; color.alpha = 0; atom.config.set('foo.bar.aColor', color); color = atom.config.get('foo.bar.aColor'); expect(color.toHexString()).toBe('#000000'); expect(color.toRGBAString()).toBe('rgba(0, 0, 0, 0)'); color.red = 300; color.green = -200; color.blue = -1; color.alpha = 'not see through'; atom.config.set('foo.bar.aColor', color); color = atom.config.get('foo.bar.aColor'); expect(color.toHexString()).toBe('#ff0000'); expect(color.toRGBAString()).toBe('rgba(255, 0, 0, 1)'); color.red = 11; color.green = 11; color.blue = 124; color.alpha = 1; atom.config.set('foo.bar.aColor', color); color = atom.config.get('foo.bar.aColor'); expect(color.toHexString()).toBe('#0b0b7c'); expect(color.toRGBAString()).toBe('rgba(11, 11, 124, 1)'); }); it('coerces various types to a color object', () => { atom.config.set('foo.bar.aColor', 'red'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 0, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', '#020'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 0, green: 34, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', '#abcdef'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 171, green: 205, blue: 239, alpha: 1 }); atom.config.set('foo.bar.aColor', 'rgb(1,2,3)'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 1, green: 2, blue: 3, alpha: 1 }); atom.config.set('foo.bar.aColor', 'rgba(4,5,6,.7)'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 4, green: 5, blue: 6, alpha: 0.7 }); atom.config.set('foo.bar.aColor', 'hsl(120,100%,50%)'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 0, green: 255, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', 'hsla(120,100%,50%,0.3)'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 0, green: 255, blue: 0, alpha: 0.3 }); atom.config.set('foo.bar.aColor', { red: 100, green: 255, blue: 2, alpha: 0.5 }); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 100, green: 255, blue: 2, alpha: 0.5 }); atom.config.set('foo.bar.aColor', { red: 255 }); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 0, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', { red: 1000 }); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 0, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', { red: 'dark' }); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 0, green: 0, blue: 0, alpha: 1 }); }); it('reverts back to the default value when undefined is passed to set', () => { atom.config.set('foo.bar.aColor', undefined); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); }); it('will not set non-colors', () => { atom.config.set('foo.bar.aColor', null); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); atom.config.set('foo.bar.aColor', 'nope'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); atom.config.set('foo.bar.aColor', 30); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); atom.config.set('foo.bar.aColor', false); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); }); it('returns a clone of the Color when returned in a parent object', () => { const color1 = atom.config.get('foo.bar').aColor; const color2 = atom.config.get('foo.bar').aColor; expect(color1.toRGBAString()).toBe('rgba(255, 255, 255, 1)'); expect(color2.toRGBAString()).toBe('rgba(255, 255, 255, 1)'); expect(color1).not.toBe(color2); expect(color1).toEqual(color2); }); }); describe('when the `enum` key is used', () => { beforeEach(() => { const schema = { type: 'object', properties: { str: { type: 'string', default: 'ok', enum: ['ok', 'one', 'two'] }, int: { type: 'integer', default: 2, enum: [2, 3, 5] }, arr: { type: 'array', default: ['one', 'two'], items: { type: 'string', enum: ['one', 'two', 'three'] } }, str_options: { type: 'string', default: 'one', enum: [ { value: 'one', description: 'One' }, 'two', { value: 'three', description: 'Three' } ] } } }; atom.config.setSchema('foo.bar', schema); }); it('will only set a string when the string is in the enum values', () => { expect(atom.config.set('foo.bar.str', 'nope')).toBe(false); expect(atom.config.get('foo.bar.str')).toBe('ok'); expect(atom.config.set('foo.bar.str', 'one')).toBe(true); expect(atom.config.get('foo.bar.str')).toBe('one'); }); it('will only set an integer when the integer is in the enum values', () => { expect(atom.config.set('foo.bar.int', '400')).toBe(false); expect(atom.config.get('foo.bar.int')).toBe(2); expect(atom.config.set('foo.bar.int', '3')).toBe(true); expect(atom.config.get('foo.bar.int')).toBe(3); }); it('will only set an array when the array values are in the enum values', () => { expect(atom.config.set('foo.bar.arr', ['one', 'five'])).toBe(true); expect(atom.config.get('foo.bar.arr')).toEqual(['one']); expect(atom.config.set('foo.bar.arr', ['two', 'three'])).toBe(true); expect(atom.config.get('foo.bar.arr')).toEqual(['two', 'three']); }); it('will honor the enum when specified as an array', () => { expect(atom.config.set('foo.bar.str_options', 'one')).toBe(true); expect(atom.config.get('foo.bar.str_options')).toEqual('one'); expect(atom.config.set('foo.bar.str_options', 'two')).toBe(true); expect(atom.config.get('foo.bar.str_options')).toEqual('two'); expect(atom.config.set('foo.bar.str_options', 'One')).toBe(false); expect(atom.config.get('foo.bar.str_options')).toEqual('two'); }); }); }); describe('when .set/.unset is called prior to .resetUserSettings', () => { beforeEach(() => { atom.config.settingsLoaded = false; }); it('ensures that early set and unset calls are replayed after the config is loaded from disk', () => { atom.config.unset('foo.bar'); atom.config.set('foo.qux', 'boo'); expect(atom.config.get('foo.bar')).toBeUndefined(); expect(atom.config.get('foo.qux')).toBe('boo'); expect(atom.config.get('do.ray')).toBeUndefined(); advanceClock(100); expect(savedSettings.length).toBe(0); atom.config.resetUserSettings({ '*': { foo: { bar: 'baz' }, do: { ray: 'me' } } }); advanceClock(100); expect(savedSettings.length).toBe(1); expect(atom.config.get('foo.bar')).toBeUndefined(); expect(atom.config.get('foo.qux')).toBe('boo'); expect(atom.config.get('do.ray')).toBe('me'); }); }); describe('project specific settings', () => { describe('config.resetProjectSettings', () => { it('gracefully handles invalid config objects', () => { atom.config.resetProjectSettings({}); expect(atom.config.get('foo.bar')).toBeUndefined(); }); }); describe('config.get', () => { const dummyPath = '/Users/dummy/path.json'; describe('project settings', () => { it('returns a deep clone of the property value', () => { atom.config.resetProjectSettings( { '*': { value: { array: [1, { b: 2 }, 3] } } }, dummyPath ); const retrievedValue = atom.config.get('value'); retrievedValue.array[0] = 4; retrievedValue.array[1].b = 2.1; expect(atom.config.get('value')).toEqual({ array: [1, { b: 2 }, 3] }); }); it('properly gets project settings', () => { atom.config.resetProjectSettings({ '*': { foo: 'wei' } }, dummyPath); expect(atom.config.get('foo')).toBe('wei'); atom.config.resetProjectSettings( { '*': { foo: { bar: 'baz' } } }, dummyPath ); expect(atom.config.get('foo.bar')).toBe('baz'); }); it('gets project settings with higher priority than regular settings', () => { atom.config.set('foo', 'bar'); atom.config.resetProjectSettings({ '*': { foo: 'baz' } }, dummyPath); expect(atom.config.get('foo')).toBe('baz'); }); it('correctly gets nested and scoped properties for project settings', () => { expect(atom.config.set('foo.bar.str', 'global')).toBe(true); expect( atom.config.set('foo.bar.str', 'scoped', { scopeSelector: '.source.js' }) ).toBe(true); expect(atom.config.get('foo.bar.str')).toBe('global'); expect( atom.config.get('foo.bar.str', { scope: ['.source.js'] }) ).toBe('scoped'); }); it('returns a deep clone of the property value', () => { atom.config.set('value', { array: [1, { b: 2 }, 3] }); const retrievedValue = atom.config.get('value'); retrievedValue.array[0] = 4; retrievedValue.array[1].b = 2.1; expect(atom.config.get('value')).toEqual({ array: [1, { b: 2 }, 3] }); }); it('gets scoped values correctly', () => { atom.config.set('foo', 'bam', { scope: ['second'] }); expect(atom.config.get('foo', { scopeSelector: 'second' })).toBe( 'bam' ); atom.config.resetProjectSettings( { '*': { foo: 'baz' }, second: { foo: 'bar' } }, dummyPath ); expect(atom.config.get('foo', { scopeSelector: 'second' })).toBe( 'baz' ); atom.config.clearProjectSettings(); expect(atom.config.get('foo', { scopeSelector: 'second' })).toBe( 'bam' ); }); it('clears project settings correctly', () => { atom.config.set('foo', 'bar'); expect(atom.config.get('foo')).toBe('bar'); atom.config.resetProjectSettings( { '*': { foo: 'baz' }, second: { foo: 'bar' } }, dummyPath ); expect(atom.config.get('foo')).toBe('baz'); expect(atom.config.getSources().length).toBe(1); atom.config.clearProjectSettings(); expect(atom.config.get('foo')).toBe('bar'); expect(atom.config.getSources().length).toBe(0); }); }); }); describe('config.getAll', () => { const dummyPath = '/Users/dummy/path.json'; it('gets settings in the same way .get would return them', () => { atom.config.resetProjectSettings({ '*': { a: 'b' } }, dummyPath); atom.config.set('a', 'f'); expect(atom.config.getAll('a')).toEqual([ { scopeSelector: '*', value: 'b' } ]); }); }); }); });