describe('PrettyPrinter', function() { it('should wrap strings in single quotes', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp('some string')).toEqual("'some string'"); expect(pp("som' string")).toEqual("'som' string'"); }); it('stringifies empty string primitives and objects recognizably', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp(new String(''))).toEqual(pp('')); expect(pp(new String(''))).toEqual("''"); expect(pp([new String('')])).toEqual(pp([''])); expect(pp([new String('')])).toEqual("[ '' ]"); }); it('should stringify primitives properly', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp(true)).toEqual('true'); expect(pp(false)).toEqual('false'); expect(pp(null)).toEqual('null'); expect(pp(jasmine.undefined)).toEqual('undefined'); expect(pp(3)).toEqual('3'); expect(pp(-3.14)).toEqual('-3.14'); expect(pp(-0)).toEqual('-0'); }); describe('stringify sets', function() { it('should stringify sets properly', function() { const set = new Set(); set.add(1); set.add(2); const pp = privateUnderTest.makePrettyPrinter(); expect(pp(set)).toEqual('Set( 1, 2 )'); }); it('should truncate sets with more elements than jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH', function() { const originalMaxSize = jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH; try { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = 2; const set = new Set(); set.add('a'); set.add('b'); set.add('c'); const pp = privateUnderTest.makePrettyPrinter(); expect(pp(set)).toEqual("Set( 'a', 'b', ... )"); } finally { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = originalMaxSize; } }); }); describe('stringify maps', function() { it('should stringify maps properly', function() { const map = new Map(); map.set(1, 2); const pp = privateUnderTest.makePrettyPrinter(); expect(pp(map)).toEqual('Map( [ 1, 2 ] )'); }); it('should truncate maps with more elements than jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH', function() { const originalMaxSize = jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH; try { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = 2; const map = new Map(); map.set('a', 1); map.set('b', 2); map.set('c', 3); const pp = privateUnderTest.makePrettyPrinter(); expect(pp(map)).toEqual("Map( [ 'a', 1 ], [ 'b', 2 ], ... )"); } finally { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = originalMaxSize; } }); }); describe('stringify arrays', function() { it('should stringify arrays properly', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp([1, 2])).toEqual('[ 1, 2 ]'); expect(pp([1, 'foo', {}, jasmine.undefined, null])).toEqual( "[ 1, 'foo', Object({ }), undefined, null ]" ); }); it('includes symbols', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp([1, Symbol('foo'), 2])).toEqual('[ 1, Symbol(foo), 2 ]'); }); it('should truncate arrays that are longer than jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH', function() { const originalMaxLength = jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH; const array = [1, 2, 3]; const pp = privateUnderTest.makePrettyPrinter(); try { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = 2; expect(pp(array)).toEqual('[ 1, 2, ... ]'); } finally { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = originalMaxLength; } }); it('should stringify arrays with properties properly', function() { const pp = privateUnderTest.makePrettyPrinter(); const arr = [1, 2]; arr.foo = 'bar'; arr.baz = {}; expect(pp(arr)).toEqual("[ 1, 2, foo: 'bar', baz: Object({ }) ]"); }); it('should stringify empty arrays with properties properly', function() { const pp = privateUnderTest.makePrettyPrinter(); const empty = []; empty.foo = 'bar'; empty.baz = {}; expect(pp(empty)).toEqual("[ foo: 'bar', baz: Object({ }) ]"); }); it('should stringify long arrays with properties properly', function() { const pp = privateUnderTest.makePrettyPrinter(); const originalMaxLength = jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH; const long = [1, 2, 3]; long.foo = 'bar'; long.baz = {}; try { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = 2; expect(pp(long)).toEqual( "[ 1, 2, ..., foo: 'bar', baz: Object({ }) ]" ); } finally { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = originalMaxLength; } }); it('should indicate circular array references', function() { const pp = privateUnderTest.makePrettyPrinter(); const array1 = [1, 2]; const array2 = [array1]; array1.push(array2); expect(pp(array1)).toEqual('[ 1, 2, [ ] ]'); }); it('should not indicate circular references incorrectly', function() { const pp = privateUnderTest.makePrettyPrinter(); const array = [[1]]; expect(pp(array)).toEqual('[ [ 1 ] ]'); }); }); it('should stringify objects properly', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp({ foo: 'bar' })).toEqual("Object({ foo: 'bar' })"); expect( pp({ foo: 'bar', baz: 3, nullValue: null, undefinedValue: jasmine.undefined }) ).toEqual( "Object({ foo: 'bar', baz: 3, nullValue: null, undefinedValue: undefined })" ); expect(pp({ foo: function() {}, bar: [1, 2, 3] })).toEqual( "Object({ foo: Function 'foo', bar: [ 1, 2, 3 ] })" ); }); it('includes symbol keys in objects', function() { const pp = privateUnderTest.makePrettyPrinter(); const obj = {}; obj[Symbol('foo')] = 'bar'; expect(pp(obj)).toEqual("Object({ Symbol(foo): 'bar' })"); }); it('stringifies string and symbol keys differently', function() { const pp = privateUnderTest.makePrettyPrinter(); const symObj = {}; const strObj = {}; const k = 'foo'; const v = 'bar'; symObj[Symbol(k)] = v; strObj[k] = v; expect(pp(symObj)).not.toEqual(pp(strObj)); }); it('should stringify objects that almost look like DOM nodes', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp({ nodeType: 1 })).toEqual('Object({ nodeType: 1 })'); }); it('should truncate objects with too many keys', function() { const pp = privateUnderTest.makePrettyPrinter(); const originalMaxLength = jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH; const long = { a: 1, b: 2, c: 3 }; try { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = 2; expect(pp(long)).toEqual('Object({ a: 1, b: 2, ... })'); } finally { jasmineUnderTest.MAX_PRETTY_PRINT_ARRAY_LENGTH = originalMaxLength; } }); function withMaxChars(maxChars, fn) { const originalMaxChars = jasmineUnderTest.MAX_PRETTY_PRINT_CHARS; try { jasmineUnderTest.MAX_PRETTY_PRINT_CHARS = maxChars; fn(); } finally { jasmineUnderTest.MAX_PRETTY_PRINT_CHARS = originalMaxChars; } } it('should truncate outputs that are too long', function() { const pp = privateUnderTest.makePrettyPrinter(); const big = [{ a: 1, b: 'a long string' }, {}]; withMaxChars(34, function() { expect(pp(big)).toEqual("[ Object({ a: 1, b: 'a long st ..."); }); }); it('should not serialize more objects after hitting MAX_PRETTY_PRINT_CHARS', function() { const a = { jasmineToString: function() { return 'object a'; } }; const b = { jasmineToString: function() { return 'object b'; } }; const c = { jasmineToString: jasmine .createSpy('c jasmineToString') .and.returnValue('') }; const d = { jasmineToString: jasmine .createSpy('d jasmineToString') .and.returnValue('') }; const pp = privateUnderTest.makePrettyPrinter(); withMaxChars(30, function() { pp([{ a: a, b: b, c: c }, d]); expect(c.jasmineToString).not.toHaveBeenCalled(); expect(d.jasmineToString).not.toHaveBeenCalled(); }); }); it("should print 'null' as the constructor of an object with its own constructor property", function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp({ constructor: function() {} })).toContain('null({'); expect(pp({ constructor: 'foo' })).toContain('null({'); }); it('should not include inherited properties when stringifying an object', function() { const pp = privateUnderTest.makePrettyPrinter(); const SomeClass = function SomeClass() {}; SomeClass.prototype.foo = 'inherited foo'; const instance = new SomeClass(); instance.bar = 'my own bar'; expect(pp(instance)).toEqual("SomeClass({ bar: 'my own bar' })"); }); it('should not recurse objects and arrays more deeply than jasmineUnderTest.MAX_PRETTY_PRINT_DEPTH', function() { const pp = privateUnderTest.makePrettyPrinter(); const originalMaxDepth = jasmineUnderTest.MAX_PRETTY_PRINT_DEPTH; const nestedObject = { level1: { level2: { level3: { level4: 'leaf' } } } }; const nestedArray = [1, [2, [3, [4, 'leaf']]]]; try { jasmineUnderTest.MAX_PRETTY_PRINT_DEPTH = 2; expect(pp(nestedObject)).toEqual( 'Object({ level1: Object({ level2: Object }) })' ); expect(pp(nestedArray)).toEqual('[ 1, [ 2, Array ] ]'); jasmineUnderTest.MAX_PRETTY_PRINT_DEPTH = 3; expect(pp(nestedObject)).toEqual( 'Object({ level1: Object({ level2: Object({ level3: Object }) }) })' ); expect(pp(nestedArray)).toEqual('[ 1, [ 2, [ 3, Array ] ] ]'); jasmineUnderTest.MAX_PRETTY_PRINT_DEPTH = 4; expect(pp(nestedObject)).toEqual( "Object({ level1: Object({ level2: Object({ level3: Object({ level4: 'leaf' }) }) }) })" ); expect(pp(nestedArray)).toEqual("[ 1, [ 2, [ 3, [ 4, 'leaf' ] ] ] ]"); } finally { jasmineUnderTest.MAX_PRETTY_PRINT_DEPTH = originalMaxDepth; } }); it('should stringify immutable circular objects', function() { const pp = privateUnderTest.makePrettyPrinter(); let frozenObject = { foo: { bar: 'baz' } }; frozenObject.circular = frozenObject; frozenObject = Object.freeze(frozenObject); expect(pp(frozenObject)).toEqual( "Object({ foo: Object({ bar: 'baz' }), circular: })" ); }); it('should stringify RegExp objects properly', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp(/x|y|z/)).toEqual('/x|y|z/'); }); it('should indicate circular object references', function() { const pp = privateUnderTest.makePrettyPrinter(); const sampleValue = { foo: 'hello' }; sampleValue.nested = sampleValue; expect(pp(sampleValue)).toEqual( "Object({ foo: 'hello', nested: })" ); }); it('should use the return value of getters', function() { const pp = privateUnderTest.makePrettyPrinter(); const sampleValue = { id: 1, get calculatedValue() { return 'the getter return value'; } }; expect(pp(sampleValue)).toEqual( "Object({ id: 1, calculatedValue: 'the getter return value' })" ); }); it('should not do HTML escaping of strings', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp('some html string &', false)).toEqual( "'some html string &'" ); }); it('should abbreviate the global (usually window) object', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp(jasmine.getGlobal())).toEqual(''); }); it('should stringify Date objects properly', function() { const pp = privateUnderTest.makePrettyPrinter(); const now = new Date(); expect(pp(now)).toEqual('Date(' + now.toString() + ')'); }); describe('with a spy object', function() { let env, pp; beforeEach(function() { env = new privateUnderTest.Env(); pp = privateUnderTest.makePrettyPrinter(); }); afterEach(function() { env.cleanup_(); }); it('should stringify spy objects properly', function() { const TestObject = { someFunction: function() {} }; const spyRegistry = new privateUnderTest.SpyRegistry({ currentSpies: function() { return []; }, createSpy: function(name, originalFn) { return privateUnderTest.Spy(name, originalFn); } }); spyRegistry.spyOn(TestObject, 'someFunction'); expect(pp(TestObject.someFunction)).toEqual('spy on someFunction'); expect(pp(env.createSpy('something'))).toEqual('spy on something'); }); it('should stringify spyOn toString properly', function() { const TestObject = { someFunction: function() {} }; const env = new privateUnderTest.Env(); const pp = privateUnderTest.makePrettyPrinter(); const spyRegistry = new privateUnderTest.SpyRegistry({ currentSpies: function() { return []; }, createSpy: function(name, originalFn) { return privateUnderTest.Spy(name, originalFn); } }); spyRegistry.spyOn(TestObject, 'toString'); const testSpyObj = env.createSpyObj('TheClassName', ['toString']); expect(pp(testSpyObj)).toEqual('spy on TheClassName.toString'); }); }); it('should stringify objects that implement jasmineToString', function() { const pp = privateUnderTest.makePrettyPrinter(); const obj = { jasmineToString: function() { return 'strung'; } }; expect(pp(obj)).toEqual('strung'); }); it('should pass itself to jasmineToString', function() { const pp = privateUnderTest.makePrettyPrinter([]); const obj = { jasmineToString: jasmine.createSpy('jasmineToString').and.returnValue('') }; pp(obj); expect(obj.jasmineToString).toHaveBeenCalledWith(pp); }); it('should stringify objects that implement custom toString', function() { const pp = privateUnderTest.makePrettyPrinter(); const obj = { toString: function() { return 'my toString'; } }; expect(pp(obj)).toEqual('my toString'); // Simulate object from another global context (e.g. an iframe or Web Worker) that does not actually have a custom // toString despite obj.toString !== Object.prototype.toString const objFromOtherContext = { foo: 'bar', toString: function() { return Object.prototype.toString.call(this); } }; expect(pp(objFromOtherContext)).toEqual( "Object({ foo: 'bar', toString: Function 'toString' })" ); }); it("should stringify objects have have a toString that isn't a function", function() { const pp = privateUnderTest.makePrettyPrinter(); const obj = { toString: 'foo' }; expect(pp(obj)).toEqual("Object({ toString: 'foo' })"); }); it('should stringify objects from anonymous constructors with custom toString', function() { const pp = privateUnderTest.makePrettyPrinter(); const MyAnonymousConstructor = (function() { return function() {}; })(); MyAnonymousConstructor.toString = function() { return ''; }; const a = new MyAnonymousConstructor(); expect(pp(a)).toEqual('({ })'); }); it('stringifies functions with names', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp(foo)).toEqual("Function 'foo'"); function foo() {} }); it('stringifies functions without names', function() { const pp = privateUnderTest.makePrettyPrinter(); expect(pp(function() {})).toEqual('Function'); }); it('should handle objects with null prototype', function() { const pp = privateUnderTest.makePrettyPrinter(); const obj = Object.create(null); obj.foo = 'bar'; expect(pp(obj)).toEqual("null({ foo: 'bar' })"); }); it('should gracefully handle objects with invalid toString implementations', function() { const pp = privateUnderTest.makePrettyPrinter(); const obj = { foo: { toString: function() { // Invalid: toString returning a number return 3; } }, bar: { toString: function() { // Really invalid: a nested bad toString(). return { toString: function() { return new Date(); } }; } }, // Valid: an actual number baz: 3, // Valid: an actual Error object qux: new Error('bar'), // baddy: { toString: function() { throw new Error('I am a bad toString'); } } }; expect(pp(obj)).toEqual( 'Object({ foo: [object Number], bar: [object Object], baz: 3, qux: Error: bar, baddy: has-invalid-toString-method })' ); }); describe('Custom object formatters', function() { it('should use the first custom object formatter that does not return undefined', function() { const customObjectFormatters = [ function() { return undefined; }, function(obj) { return '2nd: ' + obj.foo; }, function(obj) { return '3rd: ' + obj.foo; } ]; const pp = privateUnderTest.makePrettyPrinter(customObjectFormatters); const obj = { foo: 'bar' }; expect(pp(obj)).toEqual('2nd: bar'); }); it('should fall back to built in logic if all custom object formatters return undefined', function() { const customObjectFormatters = [ function() { return undefined; } ]; const pp = privateUnderTest.makePrettyPrinter(customObjectFormatters); const obj = { foo: 'bar' }; expect(pp(obj)).toEqual("Object({ foo: 'bar' })"); }); }); describe('#customFormat_', function() { it('should use the first custom object formatter that does not return undefined', function() { const customObjectFormatters = [ function() { return undefined; }, function(obj) { return '2nd: ' + obj.foo; }, function(obj) { return '3rd: ' + obj.foo; } ]; const pp = privateUnderTest.makePrettyPrinter(customObjectFormatters); const obj = { foo: 'bar' }; expect(pp.customFormat_(obj)).toEqual('2nd: bar'); }); it('should return undefined if all custom object formatters return undefined', function() { const customObjectFormatters = [ function() { return undefined; } ]; const pp = privateUnderTest.makePrettyPrinter(customObjectFormatters); const obj = { foo: 'bar' }; expect(pp.customFormat_(obj)).toBeUndefined(); }); }); });