From 2151dfbb24e704699d5409d45aa80d6cd8ab2647 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Mon, 13 Jun 2016 08:23:23 -0700 Subject: [PATCH] Open source jest tests for open source components Reviewed By: bestander Differential Revision: D3424368 fbshipit-source-id: 116229b64ecc7d8846e803e29fad377a4fb800bb --- .../Utilities/__tests__/MessageQueue-test.js | 189 +++++++++++++----- .../__tests__/MessageQueueTestConfig.js | 78 ++++++++ .../__tests__/MessageQueueTestModule1.js | 26 +++ .../__tests__/MessageQueueTestModule2.js | 20 ++ .../deepFreezeAndThrowOnMutationInDev-test.js | 121 +++++++++++ .../Utilities/__tests__/groupByEveryN-test.js | 40 ++++ .../Utilities/__tests__/truncate-test.js | 58 ++++++ 7 files changed, 483 insertions(+), 49 deletions(-) create mode 100644 Libraries/Utilities/__tests__/MessageQueueTestConfig.js create mode 100644 Libraries/Utilities/__tests__/MessageQueueTestModule1.js create mode 100644 Libraries/Utilities/__tests__/MessageQueueTestModule2.js create mode 100644 Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-test.js create mode 100644 Libraries/Utilities/__tests__/groupByEveryN-test.js create mode 100644 Libraries/Utilities/__tests__/truncate-test.js diff --git a/Libraries/Utilities/__tests__/MessageQueue-test.js b/Libraries/Utilities/__tests__/MessageQueue-test.js index 94aee83a9..32d53182f 100644 --- a/Libraries/Utilities/__tests__/MessageQueue-test.js +++ b/Libraries/Utilities/__tests__/MessageQueue-test.js @@ -1,44 +1,65 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) 2013-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. + * */ 'use strict'; -jest.unmock('MessageQueue') - .unmock('fbjs/lib/keyMirror'); -var MessageQueue = require('MessageQueue'); +const MessageQueueTestConfig = require('./MessageQueueTestConfig'); +jest.unmock('MessageQueue'); -let MODULE_IDS = 0; -let METHOD_IDS = 1; -let PARAMS = 2; +let MessageQueue; +let MessageQueueTestModule1; +let MessageQueueTestModule2; +let queue; -let TestModule = { - testHook1(){}, testHook2(){}, -}; +const MODULE_IDS = 0; +const METHOD_IDS = 1; +const PARAMS = 2; -let assertQueue = (flushedQueue, index, moduleID, methodID, params) => { +const assertQueue = (flushedQueue, index, moduleID, methodID, params) => { expect(flushedQueue[MODULE_IDS][index]).toEqual(moduleID); expect(flushedQueue[METHOD_IDS][index]).toEqual(methodID); expect(flushedQueue[PARAMS][index]).toEqual(params); }; -var queue; - -describe('MessageQueue', () => { - - beforeEach(() => { +// Important things to test: +// +// [x] Calling remote method on queue actually queues it up. +// +// [x] Both error and success callbacks are invoked. +// +// [x] When simulating an error callback from remote method, both error and +// success callbacks are cleaned up. +// +// [x] Local modules can be invoked through the queue. +// +// [ ] Local modules that throw exceptions are gracefully caught. In that case +// local callbacks stored by IDs are cleaned up. +// +// [ ] Remote invocation throws if not supplying an error callback. +describe('MessageQueue', function() { + beforeEach(function() { + jest.resetModuleRegistry(); + MessageQueue = require('MessageQueue'); + MessageQueueTestModule1 = require('./MessageQueueTestModule1'); + MessageQueueTestModule2 = require('./MessageQueueTestModule2'); queue = new MessageQueue( - () => ({ remoteModuleConfig: remoteModulesConfig }) + () => MessageQueueTestConfig ); - queue.registerCallableModule('one', TestModule); - - TestModule.testHook1 = jasmine.createSpy(); - TestModule.testHook2 = jasmine.createSpy(); + queue.registerCallableModule( + 'MessageQueueTestModule1', + MessageQueueTestModule1 + ); + queue.registerCallableModule( + 'MessageQueueTestModule2', + MessageQueueTestModule2 + ); }); it('should enqueue native calls', () => { @@ -48,59 +69,129 @@ describe('MessageQueue', () => { }); it('should call a local function with the function name', () => { - expect(TestModule.testHook2.calls.count()).toEqual(0); - queue.__callFunction('one', 'testHook2', [2]); - expect(TestModule.testHook2.calls.count()).toEqual(1); + MessageQueueTestModule1.testHook2 = jasmine.createSpy(); + expect(MessageQueueTestModule1.testHook2.calls.count()).toEqual(0); + queue.__callFunction('MessageQueueTestModule1', 'testHook2', [2]); + expect(MessageQueueTestModule1.testHook2.calls.count()).toEqual(1); }); it('should generate native modules', () => { - queue.RemoteModules.one.remoteMethod1('foo'); + queue.RemoteModules.RemoteModule1.remoteMethod1('foo'); let flushedQueue = queue.flushedQueue(); assertQueue(flushedQueue, 0, 0, 0, ['foo']); }); it('should store callbacks', () => { - queue.RemoteModules.one.remoteMethod2('foo', () => {}, () => {}); + queue.RemoteModules.RemoteModule1.remoteMethod2('foo', () => {}, () => {}); let flushedQueue = queue.flushedQueue(); assertQueue(flushedQueue, 0, 0, 1, ['foo', 0, 1]); }); it('should call the stored callback', () => { var done = false; - queue.RemoteModules.one.remoteMethod1(() => { done = true; }); + queue.RemoteModules.RemoteModule1.remoteMethod1(() => { done = true; }); queue.__invokeCallback(1); expect(done).toEqual(true); }); it('should throw when calling the same callback twice', () => { - queue.RemoteModules.one.remoteMethod1(() => {}); + queue.RemoteModules.RemoteModule1.remoteMethod1(() => {}); queue.__invokeCallback(1); expect(() => queue.__invokeCallback(1)).toThrow(); }); it('should throw when calling both success and failure callback', () => { - queue.RemoteModules.one.remoteMethod1(() => {}, () => {}); + queue.RemoteModules.RemoteModule1.remoteMethod1(() => {}, () => {}); queue.__invokeCallback(1); expect(() => queue.__invokeCallback(0)).toThrow(); }); + + it('should make round trip and clear memory', function() { + // Perform communication + + // First we're going to call into this (overriden) test hook pretending to + // be a remote module making a "local" invocation into JS. + let onFail = jasmine.createSpy(); + let onSucc = jasmine.createSpy(); + MessageQueueTestModule1.testHook1 = function() { + // Then inside of this local module, we're going to fire off a remote + // request. + queue.__nativeCall( + 0, + 0, + Array.prototype.slice.apply(arguments), + onFail, + onSucc, + ); + }; + + // The second test hook does the same thing as the first, but fires off a + // remote request to a different remote module/method. + MessageQueueTestModule1.testHook2 = function() { + queue.__nativeCall( + 1, + 1, + Array.prototype.slice.apply(arguments), + onFail, + onSucc, + ); + }; + + /* MessageQueueTestModule1.testHook1 */ + queue.__callFunction('MessageQueueTestModule1', 'testHook1', ['paloAlto', 'menloPark']); + /* MessageQueueTestModule1.testHook2 */ + queue.__callFunction('MessageQueueTestModule1', 'testHook2', ['mac', 'windows']); + + // And how do we know that it executed those local modules correctly? Well, + // these particular test method echo their arguments back to remote methods! + + var resultingRemoteInvocations = queue.flushedQueue(); + // As always, the message queue has five fields + expect(resultingRemoteInvocations.length).toBe(4); + expect(resultingRemoteInvocations[0].length).toBe(2); + expect(resultingRemoteInvocations[1].length).toBe(2); + expect(resultingRemoteInvocations[2].length).toBe(2); + expect(typeof resultingRemoteInvocations[3]).toEqual('number'); + + expect(resultingRemoteInvocations[0][0]).toBe(0); // `RemoteModule1` + expect(resultingRemoteInvocations[1][0]).toBe(0); // `remoteMethod1` + expect([ // the arguments + resultingRemoteInvocations[2][0][0], + resultingRemoteInvocations[2][0][1] + ]).toEqual(['paloAlto', 'menloPark']); + // Callbacks ids are tacked onto the end of the remote arguments. + var firstFailCBID = resultingRemoteInvocations[2][0][2]; + var firstSuccCBID = resultingRemoteInvocations[2][0][3]; + + expect(resultingRemoteInvocations[0][1]).toBe(1); // `RemoteModule2` + expect(resultingRemoteInvocations[1][1]).toBe(1); // `remoteMethod2` + expect([ // the arguments + resultingRemoteInvocations[2][1][0], + resultingRemoteInvocations[2][1][1] + ]).toEqual(['mac', 'windows']); + var secondFailCBID = resultingRemoteInvocations[2][1][2]; + var secondSuccCBID = resultingRemoteInvocations[2][1][3]; + + // Trigger init + queue.RemoteModules + // Handle the first remote invocation by signaling failure. + // ------------------------------------------------------- + queue.__invokeCallback(firstFailCBID, ['firstFailure']); + // The failure callback was already invoked, the success is no longer valid + expect(function() { + queue.__invokeCallback(firstSuccCBID, ['firstSucc']); + }).toThrow(); + expect(onFail.calls.count()).toBe(1); + expect(onSucc.calls.count()).toBe(0); + + // Handle the second remote invocation by signaling success. + // ------------------------------------------------------- + queue.__invokeCallback(secondSuccCBID, ['secondSucc']); + // The success callback was already invoked, the fail cb is no longer valid + expect(function() { + queue.__invokeCallback(secondFailCBID, ['secondFail']); + }).toThrow(); + expect(onFail.calls.count()).toBe(1); + expect(onSucc.calls.count()).toBe(1); + }); }); - -var remoteModulesConfig = { - 'one': { - 'moduleID':0, - 'methods': { - 'remoteMethod1':{ 'type': 'remote', 'methodID': 0 }, - 'remoteMethod2':{ 'type': 'remote', 'methodID': 1 }, - } - }, -}; - -var localModulesConfig = { - 'one': { - 'moduleID': 0, - 'methods': { - 'testHook1':{ 'type': 'local', 'methodID': 0 }, - 'testHook2':{ 'type': 'local', 'methodID': 1 }, - } - }, -}; diff --git a/Libraries/Utilities/__tests__/MessageQueueTestConfig.js b/Libraries/Utilities/__tests__/MessageQueueTestConfig.js new file mode 100644 index 000000000..262f9565d --- /dev/null +++ b/Libraries/Utilities/__tests__/MessageQueueTestConfig.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * These don't actually exist anywhere in the code. + */ +'use strict'; +var remoteModulesConfig = { + "RemoteModule1": { + "moduleID":0, + "methods":{ + "remoteMethod1":{ + "type":"remote", + "methodID":0 + }, + "remoteMethod2":{ + "type":"remote", + "methodID":1 + } + } + }, + "RemoteModule2":{ + "moduleID":1, + "methods":{ + "remoteMethod1":{ + "type":"remote", + "methodID":0 + }, + "remoteMethod2":{ + "type":"remote", + "methodID":1 + } + } + } +}; + +/** + * These actually exist in the __tests__ folder. + */ +var localModulesConfig = { + "MessageQueueTestModule1": { + "moduleID":"MessageQueueTestModule1", + "methods":{ + "testHook1":{ + "type":"local", + "methodID":"testHook1" + }, + "testHook2":{ + "type":"local", + "methodID":"testHook2" + } + } + }, + "MessageQueueTestModule2": { + "moduleID":"MessageQueueTestModule2", + "methods": { + "runLocalCode":{ + "type":"local", + "methodID":"runLocalCode" + }, + "runLocalCode2":{ + "type":"local", + "methodID":"runLocalCode2" + } + } + } +}; + +var MessageQueueTestConfig = { + localModuleConfig: localModulesConfig, + remoteModuleConfig: remoteModulesConfig, +}; + +module.exports = MessageQueueTestConfig; diff --git a/Libraries/Utilities/__tests__/MessageQueueTestModule1.js b/Libraries/Utilities/__tests__/MessageQueueTestModule1.js new file mode 100644 index 000000000..b076284ea --- /dev/null +++ b/Libraries/Utilities/__tests__/MessageQueueTestModule1.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule MessageQueueTestModule1 + */ +'use strict'; + +/** + * Dummy module that only exists for the sake of proving that the message queue + * correctly dispatches to commonJS modules. The `testHook` is overriden by test + * cases. + */ +var MessageQueueTestModule1 = { + testHook1: function() { + }, + testHook2: function() { + } +}; + +module.exports = MessageQueueTestModule1; + diff --git a/Libraries/Utilities/__tests__/MessageQueueTestModule2.js b/Libraries/Utilities/__tests__/MessageQueueTestModule2.js new file mode 100644 index 000000000..7a892ea6b --- /dev/null +++ b/Libraries/Utilities/__tests__/MessageQueueTestModule2.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule MessageQueueTestModule2 + */ +'use strict'; + +var MessageQueueTestModule2 = { + runLocalCode: function() { + }, + runLocalCode2: function() { + } +}; + +module.exports = MessageQueueTestModule2; diff --git a/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-test.js b/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-test.js new file mode 100644 index 000000000..4db4e749e --- /dev/null +++ b/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-test.js @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +jest.unmock('deepFreezeAndThrowOnMutationInDev'); +var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); + +describe('deepFreezeAndThrowOnMutationInDev', function() { + + it('should be a noop on non object values', () => { + __DEV__ = true; + expect(() => deepFreezeAndThrowOnMutationInDev('')).not.toThrow(); + expect(() => deepFreezeAndThrowOnMutationInDev(null)).not.toThrow(); + expect(() => deepFreezeAndThrowOnMutationInDev(false)).not.toThrow(); + expect(() => deepFreezeAndThrowOnMutationInDev(5)).not.toThrow(); + expect(() => deepFreezeAndThrowOnMutationInDev()).not.toThrow(); + __DEV__ = false; + expect(() => deepFreezeAndThrowOnMutationInDev('')).not.toThrow(); + expect(() => deepFreezeAndThrowOnMutationInDev(null)).not.toThrow(); + expect(() => deepFreezeAndThrowOnMutationInDev(false)).not.toThrow(); + expect(() => deepFreezeAndThrowOnMutationInDev(5)).not.toThrow(); + expect(() => deepFreezeAndThrowOnMutationInDev()).not.toThrow(); + }); + + it('should throw on mutation in dev with strict', () => { + 'use strict'; + __DEV__ = true; + var o = {key: 'oldValue'}; + deepFreezeAndThrowOnMutationInDev(o); + expect(() => { o.key = 'newValue'; }).toThrowError( + 'You attempted to set the key `key` with the value `"newValue"` ' + + 'on an object that is meant to be immutable and has been frozen.' + ); + expect(o.key).toBe('oldValue'); + }); + + it('should throw on mutation in dev without strict', () => { + __DEV__ = true; + var o = {key: 'oldValue'}; + deepFreezeAndThrowOnMutationInDev(o); + expect(() => { o.key = 'newValue'; }).toThrowError( + 'You attempted to set the key `key` with the value `"newValue"` ' + + 'on an object that is meant to be immutable and has been frozen.' + ); + expect(o.key).toBe('oldValue'); + }); + + it('should throw on nested mutation in dev with strict', () => { + 'use strict'; + __DEV__ = true; + var o = {key1: {key2: {key3: 'oldValue'}}}; + deepFreezeAndThrowOnMutationInDev(o); + expect(() => { o.key1.key2.key3 = 'newValue'; }).toThrowError( + 'You attempted to set the key `key3` with the value `"newValue"` ' + + 'on an object that is meant to be immutable and has been frozen.' + ); + expect(o.key1.key2.key3).toBe('oldValue'); + }); + + it('should throw on nested mutation in dev without strict', () => { + __DEV__ = true; + var o = {key1: {key2: {key3: 'oldValue'}}}; + deepFreezeAndThrowOnMutationInDev(o); + expect(() => { o.key1.key2.key3 = 'newValue'; }).toThrowError( + 'You attempted to set the key `key3` with the value `"newValue"` ' + + 'on an object that is meant to be immutable and has been frozen.' + ); + expect(o.key1.key2.key3).toBe('oldValue'); + }); + + it('should throw on insertion in dev with strict', () => { + 'use strict'; + __DEV__ = true; + var o = {oldKey: 'value'}; + deepFreezeAndThrowOnMutationInDev(o); + expect(() => { o.newKey = 'value'; }) + .toThrowError('Can\'t add property newKey, object is not extensible'); + expect(o.newKey).toBe(undefined); + }); + + it('should not throw on insertion in dev without strict', () => { + __DEV__ = true; + var o = {oldKey: 'value'}; + deepFreezeAndThrowOnMutationInDev(o); + expect(() => { o.newKey = 'value'; }).not.toThrow(); + expect(o.newKey).toBe(undefined); + }); + + it('should mutate and not throw on mutation in prod', () => { + 'use strict'; + __DEV__ = false; + var o = {key: 'oldValue'}; + deepFreezeAndThrowOnMutationInDev(o); + expect(() => { o.key = 'newValue'; }).not.toThrow(); + expect(o.key).toBe('newValue'); + }); + + // This is a limitation of the technique unfortunately + it('should not deep freeze already frozen objects', () => { + 'use strict'; + __DEV__ = true; + var o = {key1: {key2: 'oldValue'}}; + Object.freeze(o); + deepFreezeAndThrowOnMutationInDev(o); + expect(() => { o.key1.key2 = 'newValue'; }).not.toThrow(); + expect(o.key1.key2).toBe('newValue'); + }); + + it("shouldn't recurse infinitely", () => { + __DEV__ = true; + var o = {}; + o.circular = o; + deepFreezeAndThrowOnMutationInDev(o); + }); + +}); diff --git a/Libraries/Utilities/__tests__/groupByEveryN-test.js b/Libraries/Utilities/__tests__/groupByEveryN-test.js new file mode 100644 index 000000000..09fb5a11f --- /dev/null +++ b/Libraries/Utilities/__tests__/groupByEveryN-test.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +jest.unmock('groupByEveryN'); + +describe('groupByEveryN', () => { + var groupByEveryN = require('groupByEveryN'); + + it ('should group by with different n', () => { + expect(groupByEveryN([1, 2, 3, 4, 5, 6, 7, 8, 9], 1)) + .toEqual([[1], [2], [3], [4], [5], [6], [7], [8], [9]]); + expect(groupByEveryN([1, 2, 3, 4, 5, 6, 7, 8, 9], 2)) + .toEqual([[1, 2], [3, 4], [5, 6], [7, 8], [9, null]]); + expect(groupByEveryN([1, 2, 3, 4, 5, 6, 7, 8, 9], 3)) + .toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); + expect(groupByEveryN([1, 2, 3, 4, 5, 6, 7, 8, 9], 4)) + .toEqual([[1, 2, 3, 4], [5, 6, 7, 8], [9, null, null, null]]); + }); + + it ('should fill with null', () => { + expect(groupByEveryN([], 4)) + .toEqual([]); + expect(groupByEveryN([1], 4)) + .toEqual([[1, null, null, null]]); + expect(groupByEveryN([1, 2], 4)) + .toEqual([[1, 2, null, null]]); + expect(groupByEveryN([1, 2, 3], 4)) + .toEqual([[1, 2, 3, null]]); + expect(groupByEveryN([1, 2, 3, 4], 4)) + .toEqual([[1, 2, 3, 4]]); + }); +}); diff --git a/Libraries/Utilities/__tests__/truncate-test.js b/Libraries/Utilities/__tests__/truncate-test.js new file mode 100644 index 000000000..109cb6ba0 --- /dev/null +++ b/Libraries/Utilities/__tests__/truncate-test.js @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +"use strict"; + +jest.unmock('truncate'); + +describe('truncate', () => { + + var truncate = require('truncate'); + + it ('should truncate', () => { + expect(truncate('Hello, world.', 5)) + .toBe('He...'); + }); + it ('should not truncate', () => { + expect(truncate('Hello, world.', 50)) + .toBe('Hello, world.'); + }); + it ('should not truncate more than minDelta chars.', () => { + expect(truncate('Hello, world.', 7, {minDelta: 10})) + .toBe('Hello, world.'); + expect(truncate('Hello, world.', 7, {minDelta: 9})) + .toBe('Hell...'); + }); + it ('should break in the middle of words', () => { + expect(truncate('Hello, world. How are you?', 18, {breakOnWords: false})) + .toBe('Hello, world. H...'); + expect(truncate('Hello, world.\nHow are you?', 18, {breakOnWords: false})) + .toBe('Hello, world.\nHo...'); + }); + it ('should break at word boundaries', () => { + expect(truncate('Hello, world. How are you?', 18, {breakOnWords: true})) + .toBe('Hello, world....'); + expect(truncate('Hello, world.\nHow are you?', 18, {breakOnWords: true})) + .toBe('Hello, world....'); + }); + it ('should uses custom elipses', () => { + expect(truncate('Hello, world.', 9, {elipsis: '·'})) + .toBe('He·'); + }); + it ('shouldn\'t barf with weird input', () => { + expect(truncate('Hello, world.', 0)) + .toBe('Hello,...'); + expect(truncate('Hello, world.', -132)) + .toBe('...'); + expect(truncate('', 0)) + .toBe(''); + expect(truncate(null, 0)) + .toBe(null); + }); +});