diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 7c67caa3..9f7e3106 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -1217,6 +1217,18 @@ getJasmineRequireObj().Env = function(j$) { } }; + this.addCustomObjectFormatter = function(formatter) { + if (!currentRunnable()) { + throw new Error( + 'Custom object formatters must be added in a before function or a spec' + ); + } + + runnableResources[currentRunnable().id].customObjectFormatters.push( + formatter + ); + }; + j$.Expectation.addCoreMatchers(j$.matchers); j$.Expectation.addAsyncCoreMatchers(j$.asyncMatchers); @@ -1231,7 +1243,9 @@ getJasmineRequireObj().Env = function(j$) { }; var makePrettyPrinter = function() { - return j$.makePrettyPrinter(); + var customObjectFormatters = + runnableResources[currentRunnable().id].customObjectFormatters; + return j$.makePrettyPrinter(customObjectFormatters); }; var makeMatchersUtil = function() { @@ -1281,7 +1295,8 @@ getJasmineRequireObj().Env = function(j$) { customMatchers: {}, customAsyncMatchers: {}, customSpyStrategies: {}, - defaultStrategyFn: undefined + defaultStrategyFn: undefined, + customObjectFormatters: [] }; if (runnableResources[parentRunnableId]) { @@ -2318,8 +2333,8 @@ getJasmineRequireObj().ArrayContaining = function(j$) { return true; }; - ArrayContaining.prototype.jasmineToString = function () { - return ''; + ArrayContaining.prototype.jasmineToString = function (pp) { + return ''; }; return ArrayContaining; @@ -2350,8 +2365,8 @@ getJasmineRequireObj().ArrayWithExactContents = function(j$) { return true; }; - ArrayWithExactContents.prototype.jasmineToString = function() { - return ''; + ArrayWithExactContents.prototype.jasmineToString = function(pp) { + return ''; }; return ArrayWithExactContents; @@ -2433,8 +2448,8 @@ getJasmineRequireObj().MapContaining = function(j$) { return hasAllMatches; }; - MapContaining.prototype.jasmineToString = function() { - return ''; + MapContaining.prototype.jasmineToString = function(pp) { + return ''; }; return MapContaining; @@ -2510,8 +2525,8 @@ getJasmineRequireObj().ObjectContaining = function(j$) { return true; }; - ObjectContaining.prototype.jasmineToString = function() { - return ''; + ObjectContaining.prototype.jasmineToString = function(pp) { + return ''; }; return ObjectContaining; @@ -2550,8 +2565,8 @@ getJasmineRequireObj().SetContaining = function(j$) { return hasAllMatches; }; - SetContaining.prototype.jasmineToString = function() { - return ''; + SetContaining.prototype.jasmineToString = function(pp) { + return ''; }; return SetContaining; @@ -4173,14 +4188,15 @@ getJasmineRequireObj().toBeResolvedTo = function(j$) { }; getJasmineRequireObj().DiffBuilder = function(j$) { - return function DiffBuilder() { + return function DiffBuilder(config) { var path = new j$.ObjectPath(), - mismatches = []; + mismatches = [], + prettyPrinter = (config || {}).prettyPrinter || j$.makePrettyPrinter(); return { record: function (actual, expected, formatter) { formatter = formatter || defaultFormatter; - mismatches.push(formatter(actual, expected, path)); + mismatches.push(formatter(actual, expected, path, prettyPrinter)); }, getMessage: function () { @@ -4195,12 +4211,12 @@ getJasmineRequireObj().DiffBuilder = function(j$) { } }; - function defaultFormatter (actual, expected, path) { + function defaultFormatter (actual, expected, path, prettyPrinter) { return 'Expected ' + path + (path.depth() ? ' = ' : '') + - j$.pp(actual) + + prettyPrinter(actual) + ' to equal ' + - j$.pp(expected) + + prettyPrinter(expected) + '.'; } }; @@ -4253,7 +4269,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) { if (i > 0) { message += ','; } - message += ' ' + j$.pp(expected[i]); + message += ' ' + self.pp(expected[i]); } } @@ -5289,7 +5305,7 @@ getJasmineRequireObj().toEqual = function(j$) { var result = { pass: false }, - diffBuilder = j$.DiffBuilder(); + diffBuilder = j$.DiffBuilder({prettyPrinter: util.pp}); result.pass = util.equals(actual, expected, diffBuilder); @@ -5990,11 +6006,13 @@ getJasmineRequireObj().MockDate = function() { }; getJasmineRequireObj().makePrettyPrinter = function(j$) { - function SinglePrettyPrintRun() { + function SinglePrettyPrintRun(customObjectFormatters, pp) { + this.customObjectFormatters_ = customObjectFormatters; this.ppNestLevel_ = 0; this.seen = []; this.length = 0; this.stringParts = []; + this.pp_ = pp; } function hasCustomToString(value) { @@ -6015,7 +6033,11 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { SinglePrettyPrintRun.prototype.format = function(value) { this.ppNestLevel_++; try { - if (j$.util.isUndefined(value)) { + var customFormatResult = this.applyCustomFormatters_(value); + + if (customFormatResult) { + this.emitScalar(customFormatResult); + } else if (j$.util.isUndefined(value)) { this.emitScalar('undefined'); } else if (value === null) { this.emitScalar('null'); @@ -6024,7 +6046,7 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { } else if (value === j$.getGlobal()) { this.emitScalar(''); } else if (value.jasmineToString) { - this.emitScalar(value.jasmineToString()); + this.emitScalar(value.jasmineToString(this.pp_)); } else if (typeof value === 'string') { this.emitString(value); } else if (j$.isSpy(value)) { @@ -6086,6 +6108,18 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { } }; + SinglePrettyPrintRun.prototype.applyCustomFormatters_ = function(value) { + var i, result; + + for (i = 0; i < this.customObjectFormatters_.length; i++) { + result = this.customObjectFormatters_[i](value); + + if (result !== undefined) { + return result; + } + } + }; + SinglePrettyPrintRun.prototype.iterateObject = function(obj, fn) { var objKeys = keys(obj, j$.isArray_(obj)); var isGetter = function isGetter(prop) {}; @@ -6356,12 +6390,17 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { return extraKeys; } - return function() { - return function(value) { - var prettyPrinter = new SinglePrettyPrintRun(); + return function(customObjectFormatters) { + var pp = function(value) { + var prettyPrinter = new SinglePrettyPrintRun( + customObjectFormatters || [], + pp + ); prettyPrinter.format(value); return prettyPrinter.stringParts.join(''); }; + + return pp; }; }; @@ -6972,6 +7011,20 @@ getJasmineRequireObj().interface = function(jasmine, env) { return env.addAsyncMatchers(matchers); }; + /** + * Add a custom object formatter for the current scope of specs. + * + * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. + * @name jasmine.addCustomObjectFormatter + * @since 3.6.0 + * @function + * @param {Function} formatter - A function which takes a value to format and returns a string if it knows how to format it, and `undefined` otherwise. + * @see custom_object_formatter + */ + jasmine.addCustomObjectFormatter = function(formatter) { + return env.addCustomObjectFormatter(formatter); + }; + /** * Get the currently booted mock {Clock} for this Jasmine environment. * @name jasmine.clock diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index 80dcffbb..9349a78d 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -305,8 +305,9 @@ describe('Env', function() { }); }); - it('creates an expectationFactory that uses the current custom equality testers', function(done) { + it('creates an expectationFactory that uses the current custom equality testers and object formatters', function(done) { function customEqualityTester() {} + function customObjectFormatter() {} function prettyPrinter() {} var RealSpec = jasmineUnderTest.Spec, specInstance, @@ -321,11 +322,15 @@ describe('Env', function() { env.it('spec', function() { env.addCustomEqualityTester(customEqualityTester); + env.addCustomObjectFormatter(customObjectFormatter); expectationFactory('actual', specInstance); }); env.addReporter({ jasmineDone: function() { + expect(jasmineUnderTest.makePrettyPrinter).toHaveBeenCalledWith([ + customObjectFormatter + ]); expect(jasmineUnderTest.MatchersUtil).toHaveBeenCalledWith({ customTesters: [customEqualityTester], pp: prettyPrinter @@ -337,8 +342,9 @@ describe('Env', function() { env.execute(); }); - it('creates an asyncExpectationFactory that uses the current custom equality testers', function(done) { + it('creates an asyncExpectationFactory that uses the current custom equality testers and object formatters', function(done) { function customEqualityTester() {} + function customObjectFormatter() {} function prettyPrinter() {} var RealSpec = jasmineUnderTest.Spec, specInstance, @@ -353,11 +359,15 @@ describe('Env', function() { env.it('spec', function() { env.addCustomEqualityTester(customEqualityTester); + env.addCustomObjectFormatter(customObjectFormatter); asyncExpectationFactory('actual', specInstance); }); env.addReporter({ jasmineDone: function() { + expect(jasmineUnderTest.makePrettyPrinter).toHaveBeenCalledWith([ + customObjectFormatter + ]); expect(jasmineUnderTest.MatchersUtil).toHaveBeenCalledWith({ customTesters: [customEqualityTester], pp: prettyPrinter diff --git a/spec/core/PrettyPrintSpec.js b/spec/core/PrettyPrintSpec.js index 46f08d6a..4aafefcd 100644 --- a/spec/core/PrettyPrintSpec.js +++ b/spec/core/PrettyPrintSpec.js @@ -388,6 +388,16 @@ describe('PrettyPrinter', function() { expect(pp(obj)).toEqual('strung'); }); + it('should pass itself to jasmineToString', function() { + var pp = jasmineUnderTest.makePrettyPrinter([]); + var obj = { + jasmineToString: jasmine.createSpy('jasmineToString').and.returnValue('') + }; + + pp(obj); + expect(obj.jasmineToString).toHaveBeenCalledWith(pp); + }); + it('should stringify objects that implement custom toString', function() { var pp = jasmineUnderTest.makePrettyPrinter(); var obj = { @@ -478,4 +488,36 @@ describe('PrettyPrinter', function() { '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() { + var customObjectFormatters = [ + function(obj) { + return undefined; + }, + function(obj) { + return '2nd: ' + obj.foo; + }, + function(obj) { + return '3rd: ' + obj.foo; + } + ], + pp = jasmineUnderTest.makePrettyPrinter(customObjectFormatters), + obj = { foo: 'bar' }; + + expect(pp(obj, customObjectFormatters)).toEqual('2nd: bar'); + }); + + it('should fall back to built in logic if all custom object formatters return undefined', function() { + var customObjectFormatters = [ + function(obj) { + return undefined; + } + ], + pp = jasmineUnderTest.makePrettyPrinter(customObjectFormatters), + obj = { foo: 'bar' }; + + expect(pp(obj, customObjectFormatters)).toEqual("Object({ foo: 'bar' })"); + }); + }); }); diff --git a/spec/core/asymmetric_equality/ArrayContainingSpec.js b/spec/core/asymmetric_equality/ArrayContainingSpec.js index 4cbfa66d..33e411e6 100644 --- a/spec/core/asymmetric_equality/ArrayContainingSpec.js +++ b/spec/core/asymmetric_equality/ArrayContainingSpec.js @@ -42,9 +42,14 @@ describe("ArrayContaining", function() { }); it("jasmineToStrings itself", function() { - var containing = new jasmineUnderTest.ArrayContaining([]); + var sample = [], + matcher = new jasmineUnderTest.ArrayContaining(sample), + pp = jasmine.createSpy('pp').and.returnValue('sample'); - expect(containing.jasmineToString()).toMatch("' + ); + expect(pp).toHaveBeenCalledWith(sample); }); it("uses custom equality testers", function() { diff --git a/spec/core/asymmetric_equality/ArrayWithExactContentsSpec.js b/spec/core/asymmetric_equality/ArrayWithExactContentsSpec.js index 5f8795d6..fd0bb30c 100644 --- a/spec/core/asymmetric_equality/ArrayWithExactContentsSpec.js +++ b/spec/core/asymmetric_equality/ArrayWithExactContentsSpec.js @@ -30,9 +30,14 @@ describe("ArrayWithExactContents", function() { }); it("jasmineToStrings itself", function() { - var matcher = new jasmineUnderTest.ArrayWithExactContents([]); + var sample = [], + matcher = new jasmineUnderTest.ArrayWithExactContents(sample), + pp = jasmine.createSpy('pp').and.returnValue('sample'); - expect(matcher.jasmineToString()).toMatch("' + ); + expect(pp).toHaveBeenCalledWith(sample); }); it("uses custom equality testers", function() { diff --git a/spec/core/asymmetric_equality/MapContainingSpec.js b/spec/core/asymmetric_equality/MapContainingSpec.js index f264e693..c9ea83a6 100644 --- a/spec/core/asymmetric_equality/MapContainingSpec.js +++ b/spec/core/asymmetric_equality/MapContainingSpec.js @@ -184,8 +184,13 @@ describe('MapContaining', function() { }); it('defines a `jasmineToString` method', function() { - var containing = new jasmineUnderTest.MapContaining(new Map()); + var sample = new Map(), + containing = new jasmineUnderTest.MapContaining(sample), + pp = jasmine.createSpy('pp').and.returnValue('sample'); - expect(containing.jasmineToString()).toMatch(/^' + ); + expect(pp).toHaveBeenCalledWith(sample); }); }); diff --git a/spec/core/asymmetric_equality/ObjectContainingSpec.js b/spec/core/asymmetric_equality/ObjectContainingSpec.js index 13fa0a90..4fc16d6b 100644 --- a/spec/core/asymmetric_equality/ObjectContainingSpec.js +++ b/spec/core/asymmetric_equality/ObjectContainingSpec.js @@ -37,9 +37,15 @@ describe("ObjectContaining", function() { }); it("jasmineToString's itself", function() { - var containing = new jasmineUnderTest.ObjectContaining({}); + var sample = {}, + matcher = new jasmineUnderTest.ObjectContaining(sample), + pp = jasmine.createSpy('pp').and.returnValue('sample'); + + expect(matcher.jasmineToString(pp)).toEqual( + '' + ); + expect(pp).toHaveBeenCalledWith(sample); - expect(containing.jasmineToString()).toMatch("' + ); + expect(pp).toHaveBeenCalledWith(sample); }); }); diff --git a/spec/core/integration/CustomObjectFormatterSpec.js b/spec/core/integration/CustomObjectFormatterSpec.js new file mode 100644 index 00000000..10ed4d71 --- /dev/null +++ b/spec/core/integration/CustomObjectFormatterSpec.js @@ -0,0 +1,67 @@ +describe("Custom object formatters", function() { + var env; + + beforeEach(function() { + env = new jasmineUnderTest.Env(); + env.configure({random: false}); + }); + + it("scopes custom object formatters to a spec", function(done) { + env.it('a spec with custom pretty-printer', function() { + env.addCustomObjectFormatter(function(obj) { return 'custom(' + obj + ')'; }); + env.expect(42).toBeUndefined(); + }); + + env.it('a spec without custom pretty-printer', function() { + env.expect(42).toBeUndefined(); + }); + + var specResults = []; + var specDone = function(result) { + specResults.push(result); + }; + var expectations = function() { + expect(specResults[0].failedExpectations[0].message).toEqual("Expected custom(42) to be undefined."); + expect(specResults[1].failedExpectations[0].message).toEqual("Expected 42 to be undefined."); + done(); + }; + env.addReporter({ specDone:specDone, jasmineDone: expectations}); + + env.execute(); + }); + + it("scopes custom object formatters to a suite", function(done) { + env.it('a spec without custom pretty-printer', function() { + env.expect(42).toBeUndefined(); + }); + + env.describe('with custom pretty-printer', function() { + env.beforeEach(function() { + env.addCustomObjectFormatter(function(obj) { return 'custom(' + obj + ')'; }); + }); + + env.it('a spec', function() { + env.expect(42).toBeUndefined(); + }); + }); + + var specResults = []; + var specDone = function(result) { + specResults.push(result); + }; + var expectations = function() { + expect(specResults[0].failedExpectations[0].message).toEqual("Expected 42 to be undefined."); + expect(specResults[1].failedExpectations[0].message).toEqual("Expected custom(42) to be undefined."); + done(); + }; + env.addReporter({ specDone:specDone, jasmineDone: expectations}); + + env.execute(); + }); + + it("throws an exception if you try to add a custom object formatter outside a runable", function() { + expect(function() { + env.addCustomObjectFormatter(function() {}); + }).toThrowError('Custom object formatters must be added in a before function or a spec') + }); +}); diff --git a/spec/core/integration/MatchersSpec.js b/spec/core/integration/MatchersSpec.js index b21208d5..337f9eb2 100644 --- a/spec/core/integration/MatchersSpec.js +++ b/spec/core/integration/MatchersSpec.js @@ -48,6 +48,28 @@ describe('Matchers (Integration)', function() { }); } + function verifyFailsWithCustomObjectFormatters(config) { + it('uses custom object formatters', function(done) { + var env = new jasmineUnderTest.Env(); + env.it('a spec', function () { + env.addCustomObjectFormatter(config.formatter); + config.expectations(env); + }); + + var specExpectations = function (result) { + expect(result.status).toEqual('failed'); + expect(result.failedExpectations.length) + .withContext('Number of failed expectations') + .toEqual(1); + expect(result.failedExpectations[0].message) + .toEqual(config.expectedMessage); + }; + + env.addReporter({specDone: specExpectations, jasmineDone: done}); + env.execute(); + }); + } + function verifyPassesAsync(expectations) { it('passes', function(done) { jasmine.getEnv().requirePromises(); @@ -101,6 +123,30 @@ describe('Matchers (Integration)', function() { }); } + function verifyFailsWithCustomObjectFormattersAsync(config) { + it('uses custom object formatters', function(done) { + var env = new jasmineUnderTest.Env(); + jasmine.getEnv().requirePromises(); + env.it('a spec', function () { + env.addCustomObjectFormatter(config.formatter); + return config.expectations(env); + }); + + var specExpectations = function (result) { + expect(result.status).toEqual('failed'); + expect(result.failedExpectations.length) + .withContext('Number of failed expectations') + .toEqual(1); + expect(result.failedExpectations[0].message) + .toEqual(config.expectedMessage); + }; + + env.addReporter({specDone: specExpectations, jasmineDone: done}); + env.execute(); + }); + } + + describe('nothing', function() { verifyPasses(function(env) { env.expect().nothing(); @@ -217,6 +263,16 @@ describe('Matchers (Integration)', function() { verifyFails(function(env) { env.expect(2).toBeNaN(); }); + + verifyFailsWithCustomObjectFormatters({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + env.expect(1).toBeNaN(); + }, + expectedMessage: 'Expected |1| to be NaN.' + }); }); describe('toBeNegativeInfinity', function() { @@ -227,6 +283,16 @@ describe('Matchers (Integration)', function() { verifyFails(function(env) { env.expect(2).toBeNegativeInfinity(); }); + + verifyFailsWithCustomObjectFormatters({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + env.expect(1).toBeNegativeInfinity(); + }, + expectedMessage: 'Expected |1| to be -Infinity.' + }); }); describe('toBeNull', function() { @@ -247,6 +313,16 @@ describe('Matchers (Integration)', function() { verifyFails(function(env) { env.expect(2).toBePositiveInfinity(); }); + + verifyFailsWithCustomObjectFormatters({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + env.expect(1).toBePositiveInfinity(); + }, + expectedMessage: 'Expected |1| to be Infinity.' + }) }); describe('toBeResolved', function() { @@ -270,6 +346,17 @@ describe('Matchers (Integration)', function() { verifyFailsAsync(function(env) { return env.expectAsync(Promise.resolve('foo')).toBeResolvedTo('bar'); }); + + verifyFailsWithCustomObjectFormattersAsync({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + return env.expectAsync(Promise.resolve('x')).toBeResolvedTo('y'); + }, + expectedMessage: 'Expected a promise to be resolved to |y| ' + + 'but it was resolved to |x|.' + }); }); describe('toBeRejected', function() { @@ -293,6 +380,17 @@ describe('Matchers (Integration)', function() { verifyFailsAsync(function(env) { return env.expectAsync(Promise.resolve()).toBeRejectedWith('nope'); }); + + verifyFailsWithCustomObjectFormattersAsync({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + return env.expectAsync(Promise.reject('x')).toBeRejectedWith('y'); + }, + expectedMessage: 'Expected a promise to be rejected with |y| ' + + 'but it was rejected with |x|.' + }); }); describe('toBeRejectedWithError', function() { @@ -303,6 +401,17 @@ describe('Matchers (Integration)', function() { verifyFailsAsync(function(env) { return env.expectAsync(Promise.resolve()).toBeRejectedWithError(Error); }); + + verifyFailsWithCustomObjectFormattersAsync({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + return env.expectAsync(Promise.reject('foo')).toBeRejectedWithError('foo'); + }, + expectedMessage: 'Expected a promise to be rejected with Error: |foo| ' + + 'but it was rejected with |foo|.' + }); }); describe('toBeTrue', function() { @@ -359,6 +468,20 @@ describe('Matchers (Integration)', function() { verifyFails(function(env) { env.expect('a').toEqual('b'); }); + + verifyFailsWithCustomObjectFormatters({ + formatter: function(val) { + if (val === 5) { + return "five" + } else if (val === 4) { + return "four" + } + }, + expectations: function(env) { + env.expect([{foo: 4}]).toEqual([{foo: 5}]); + }, + expectedMessage: 'Expected $[0].foo = four to equal five.' + }); }); describe('toHaveBeenCalled', function() { @@ -417,6 +540,19 @@ describe('Matchers (Integration)', function() { var spy = env.createSpy(); env.expect(spy).toHaveBeenCalledWith('foo'); }); + + verifyFailsWithCustomObjectFormatters({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + var spy = env.createSpy('foo'); + env.expect(spy).toHaveBeenCalledWith('x'); + }, + expectedMessage: 'Expected spy foo to have been called with:\n' + + ' |x|\n' + + 'but it was never called.' + }); }); describe('toHaveClass', function() { @@ -458,6 +594,19 @@ describe('Matchers (Integration)', function() { verifyFails(function(env) { env.expect(function() {}).toThrow(); }); + + verifyFailsWithCustomObjectFormatters({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + var spy = env.createSpy('foo'); + env.expect(function() { + throw 'x' + }).not.toThrow(); + }, + expectedMessage: 'Expected function not to throw, but it threw |x|.' + }); }); describe('toThrowError', function() { @@ -468,6 +617,19 @@ describe('Matchers (Integration)', function() { verifyFails(function(env) { env.expect(function() { }).toThrowError(); }); + + verifyFailsWithCustomObjectFormatters({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + var spy = env.createSpy('foo'); + env.expect(function() { + throw 'x' + }).toThrowError(); + }, + expectedMessage: 'Expected function to throw an Error, but it threw |x|.' + }); }); describe('toThrowMatching', function() { @@ -482,5 +644,21 @@ describe('Matchers (Integration)', function() { verifyFails(function(env) { env.expect(throws).toThrowMatching(function() { return false; }); }); + + verifyFailsWithCustomObjectFormatters({ + formatter: function(val) { + return '|' + val + '|'; + }, + expectations: function(env) { + var spy = env.createSpy('foo'); + env.expect(function() { + throw new Error('nope') + }).toThrowMatching(function() { + return false; + }); + }, + expectedMessage: 'Expected function to throw an exception matching ' + + 'a predicate, but it threw Error with message |nope|.' + }); }); }); diff --git a/spec/core/matchers/DiffBuilderSpec.js b/spec/core/matchers/DiffBuilderSpec.js index 2fe8f1bf..3b68207d 100644 --- a/spec/core/matchers/DiffBuilderSpec.js +++ b/spec/core/matchers/DiffBuilderSpec.js @@ -44,4 +44,30 @@ describe("DiffBuilder", function() { expect(diffBuilder.getMessage()).toEqual("I find your lack of foo disturbing. (was bar, at $.x)"); }); + + it("uses the injected pretty-printer", function() { + var prettyPrinter = function(val) { + return '|' + val + '|'; + }, + diffBuilder = jasmineUnderTest.DiffBuilder({prettyPrinter: prettyPrinter}); + + diffBuilder.withPath('foo', function() { + diffBuilder.record('actual', 'expected'); + }); + + expect(diffBuilder.getMessage()).toEqual("Expected $.foo = |actual| to equal |expected|."); + }); + + + it("passes the injected pretty-printer to the diff formatter", function() { + var diffFormatter = jasmine.createSpy('diffFormatter'), + prettyPrinter = function() {}, + diffBuilder = jasmineUnderTest.DiffBuilder({prettyPrinter: prettyPrinter}); + + diffBuilder.withPath('x', function() { + diffBuilder.record('bar', 'foo', diffFormatter); + }); + + expect(diffFormatter).toHaveBeenCalledWith('bar', 'foo', jasmine.anything(), prettyPrinter); + }); }); diff --git a/spec/core/matchers/matchersUtilSpec.js b/spec/core/matchers/matchersUtilSpec.js index ff9b159d..1c0903ba 100644 --- a/spec/core/matchers/matchersUtilSpec.js +++ b/spec/core/matchers/matchersUtilSpec.js @@ -840,7 +840,7 @@ describe("matchersUtil", function() { }); }); - describe("buildMessage", function() { + describe("buildFailureMessage", function() { it("builds an English sentence for a failure case", function() { var actual = "foo", @@ -873,15 +873,17 @@ describe("matchersUtil", function() { expect(message).toEqual("Expected 'foo' to bar 'quux', 'corge'."); }); - it("uses the injected pretty-printer to format the expected", function() { + it("uses the injected pretty-printer to format the expecteds and actual", function() { var actual = "foo", + expected1 = "qux", + expected2 = "grault", name = "toBar", isNot = false, pp = function(value) { return '<' + value + '>'; }, matchersUtil = new jasmineUnderTest.MatchersUtil({pp: pp}), - message = message = matchersUtil.buildFailureMessage(name, isNot, actual); + message = message = matchersUtil.buildFailureMessage(name, isNot, actual, expected1, expected2); - expect(message).toEqual("Expected to bar."); + expect(message).toEqual("Expected to bar , ."); }); }); }); diff --git a/spec/core/matchers/toBeSpec.js b/spec/core/matchers/toBeSpec.js index d95f2b49..49484f5f 100644 --- a/spec/core/matchers/toBeSpec.js +++ b/spec/core/matchers/toBeSpec.js @@ -59,4 +59,16 @@ describe("toBe", function() { expect(result.pass).toBe(false); expect(result.message).toBe("Expected Object({ foo: 'bar' }) to be Object({ foo: 'bar' }). Tip: To check for deep equality, use .toEqual() instead of .toBe().") }); + + it("works with custom object formatters when expected is an object", function() { + var formatter = function(x) { return '<' + x.foo + '>'; }, + prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]), + util = new jasmineUnderTest.MatchersUtil({pp: prettyPrinter}), + matcher = jasmineUnderTest.matchers.toBe(util), + result; + + result = matcher.compare({foo: "bar"}, {foo: "bar"}); + expect(result.pass).toBe(false); + expect(result.message).toBe("Expected to be . Tip: To check for deep equality, use .toEqual() instead of .toBe().") + }); }); diff --git a/spec/core/matchers/toEqualSpec.js b/spec/core/matchers/toEqualSpec.js index 5702f7bb..5bfc7715 100644 --- a/spec/core/matchers/toEqualSpec.js +++ b/spec/core/matchers/toEqualSpec.js @@ -97,14 +97,34 @@ describe("toEqual", function() { expect(compareEquals(actual, expected).message).toEqual(message); }); - it("reports extra and missing properties together", function() { + it("uses custom object formatters to pretty-print properties", function() { + function formatter(x) { return '|' + x + '|'; } + var actual = {x: {y: 1, z: 2, f: 4}}, expected = {x: {y: 1, z: 2, g: 3}}, + pp = jasmineUnderTest.makePrettyPrinter([formatter]), + util = new jasmineUnderTest.MatchersUtil({pp: pp}), + matcher = jasmineUnderTest.matchers.toEqual(util), message = "Expected $.x to have properties\n" + - " g: 3\n" + + " g: |3|\n" + "Expected $.x not to have properties\n" + - " f: 4"; + " f: |4|"; + + expect(matcher.compare(actual, expected).message).toEqual(message); + }); + + it("uses custom object formatters to build diffs", function() { + function formatter(x) { return '|' + x + '|'; } + + var actual = [{foo: 4}], + expected = [{foo: 5}], + prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]), + util = new jasmineUnderTest.MatchersUtil({pp: prettyPrinter}), + matcher = jasmineUnderTest.matchers.toEqual(util), + message = "Expected $[0].foo = |4| to equal |5|."; + + expect(matcher.compare(actual, expected).message).toEqual(message); }); it("reports extra and missing properties of the root-level object", function() { @@ -277,12 +297,27 @@ describe("toEqual", function() { function Bar() {} var actual = {x: new Foo()}, - expected = {x: new Bar()}, - message = "Expected $.x to be a kind of Bar, but was Foo({ })."; + expected = {x: new Bar()}, + message = "Expected $.x to be a kind of Bar, but was Foo({ })."; expect(compareEquals(actual, expected).message).toEqual(message); }); + it("uses custom object formatters to report objects with different constructors", function () { + function Foo() {} + function Bar() {} + function formatter(x) { return '|' + x + '|'; } + + var actual = {x: new Foo()}, + expected = {x: new Bar()}, + message = "Expected $.x to be a kind of Bar, but was |[object Object]|.", + pp = jasmineUnderTest.makePrettyPrinter([formatter]), + util = new jasmineUnderTest.MatchersUtil({pp: pp}), + matcher = jasmineUnderTest.matchers.toEqual(util); + + expect(matcher.compare(actual, expected).message).toEqual(message); + }); + it("reports type mismatches at the root level", function () { function Foo() {} function Bar() {} @@ -803,6 +838,20 @@ describe("toEqual", function() { expect(compareEquals(actual, expected).message).toEqual(message); }); + it("uses custom object formatters when the actual array is longer", function() { + function formatter(x) { return '|' + x + '|'; } + + var actual = [1, 1, 2, 3, 5], + expected = [1, 1, 2, 3], + pp = jasmineUnderTest.makePrettyPrinter([formatter]), + util = new jasmineUnderTest.MatchersUtil({pp: pp}), + matcher = jasmineUnderTest.matchers.toEqual(util), + message = 'Expected $.length = |5| to equal |4|.\n' + + 'Unexpected $[4] = |5| in array.'; + + expect(matcher.compare(actual, expected).message).toEqual(message); + }); + it("expected array is longer", function() { var actual = [1, 1, 2, 3], expected = [1, 1, 2, 3, 5], diff --git a/src/core/Env.js b/src/core/Env.js index 42ba2756..62510708 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -296,6 +296,18 @@ getJasmineRequireObj().Env = function(j$) { } }; + this.addCustomObjectFormatter = function(formatter) { + if (!currentRunnable()) { + throw new Error( + 'Custom object formatters must be added in a before function or a spec' + ); + } + + runnableResources[currentRunnable().id].customObjectFormatters.push( + formatter + ); + }; + j$.Expectation.addCoreMatchers(j$.matchers); j$.Expectation.addAsyncCoreMatchers(j$.asyncMatchers); @@ -310,7 +322,9 @@ getJasmineRequireObj().Env = function(j$) { }; var makePrettyPrinter = function() { - return j$.makePrettyPrinter(); + var customObjectFormatters = + runnableResources[currentRunnable().id].customObjectFormatters; + return j$.makePrettyPrinter(customObjectFormatters); }; var makeMatchersUtil = function() { @@ -360,7 +374,8 @@ getJasmineRequireObj().Env = function(j$) { customMatchers: {}, customAsyncMatchers: {}, customSpyStrategies: {}, - defaultStrategyFn: undefined + defaultStrategyFn: undefined, + customObjectFormatters: [] }; if (runnableResources[parentRunnableId]) { diff --git a/src/core/PrettyPrinter.js b/src/core/PrettyPrinter.js index 14091e5e..a15177ce 100644 --- a/src/core/PrettyPrinter.js +++ b/src/core/PrettyPrinter.js @@ -1,9 +1,11 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { - function SinglePrettyPrintRun() { + function SinglePrettyPrintRun(customObjectFormatters, pp) { + this.customObjectFormatters_ = customObjectFormatters; this.ppNestLevel_ = 0; this.seen = []; this.length = 0; this.stringParts = []; + this.pp_ = pp; } function hasCustomToString(value) { @@ -24,7 +26,11 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { SinglePrettyPrintRun.prototype.format = function(value) { this.ppNestLevel_++; try { - if (j$.util.isUndefined(value)) { + var customFormatResult = this.applyCustomFormatters_(value); + + if (customFormatResult) { + this.emitScalar(customFormatResult); + } else if (j$.util.isUndefined(value)) { this.emitScalar('undefined'); } else if (value === null) { this.emitScalar('null'); @@ -33,7 +39,7 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { } else if (value === j$.getGlobal()) { this.emitScalar(''); } else if (value.jasmineToString) { - this.emitScalar(value.jasmineToString()); + this.emitScalar(value.jasmineToString(this.pp_)); } else if (typeof value === 'string') { this.emitString(value); } else if (j$.isSpy(value)) { @@ -95,6 +101,18 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { } }; + SinglePrettyPrintRun.prototype.applyCustomFormatters_ = function(value) { + var i, result; + + for (i = 0; i < this.customObjectFormatters_.length; i++) { + result = this.customObjectFormatters_[i](value); + + if (result !== undefined) { + return result; + } + } + }; + SinglePrettyPrintRun.prototype.iterateObject = function(obj, fn) { var objKeys = keys(obj, j$.isArray_(obj)); var isGetter = function isGetter(prop) {}; @@ -365,11 +383,16 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) { return extraKeys; } - return function() { - return function(value) { - var prettyPrinter = new SinglePrettyPrintRun(); + return function(customObjectFormatters) { + var pp = function(value) { + var prettyPrinter = new SinglePrettyPrintRun( + customObjectFormatters || [], + pp + ); prettyPrinter.format(value); return prettyPrinter.stringParts.join(''); }; + + return pp; }; }; diff --git a/src/core/asymmetric_equality/ArrayContaining.js b/src/core/asymmetric_equality/ArrayContaining.js index 95c9f3f1..7faea8b4 100644 --- a/src/core/asymmetric_equality/ArrayContaining.js +++ b/src/core/asymmetric_equality/ArrayContaining.js @@ -25,8 +25,8 @@ getJasmineRequireObj().ArrayContaining = function(j$) { return true; }; - ArrayContaining.prototype.jasmineToString = function () { - return ''; + ArrayContaining.prototype.jasmineToString = function (pp) { + return ''; }; return ArrayContaining; diff --git a/src/core/asymmetric_equality/ArrayWithExactContents.js b/src/core/asymmetric_equality/ArrayWithExactContents.js index 9bdba8d4..d2049047 100644 --- a/src/core/asymmetric_equality/ArrayWithExactContents.js +++ b/src/core/asymmetric_equality/ArrayWithExactContents.js @@ -23,8 +23,8 @@ getJasmineRequireObj().ArrayWithExactContents = function(j$) { return true; }; - ArrayWithExactContents.prototype.jasmineToString = function() { - return ''; + ArrayWithExactContents.prototype.jasmineToString = function(pp) { + return ''; }; return ArrayWithExactContents; diff --git a/src/core/asymmetric_equality/MapContaining.js b/src/core/asymmetric_equality/MapContaining.js index 7c8e3c5b..ba6b0ce9 100644 --- a/src/core/asymmetric_equality/MapContaining.js +++ b/src/core/asymmetric_equality/MapContaining.js @@ -33,8 +33,8 @@ getJasmineRequireObj().MapContaining = function(j$) { return hasAllMatches; }; - MapContaining.prototype.jasmineToString = function() { - return ''; + MapContaining.prototype.jasmineToString = function(pp) { + return ''; }; return MapContaining; diff --git a/src/core/asymmetric_equality/ObjectContaining.js b/src/core/asymmetric_equality/ObjectContaining.js index 93069b25..7d9f4cf6 100644 --- a/src/core/asymmetric_equality/ObjectContaining.js +++ b/src/core/asymmetric_equality/ObjectContaining.js @@ -41,8 +41,8 @@ getJasmineRequireObj().ObjectContaining = function(j$) { return true; }; - ObjectContaining.prototype.jasmineToString = function() { - return ''; + ObjectContaining.prototype.jasmineToString = function(pp) { + return ''; }; return ObjectContaining; diff --git a/src/core/asymmetric_equality/SetContaining.js b/src/core/asymmetric_equality/SetContaining.js index 00627935..f3ad3527 100644 --- a/src/core/asymmetric_equality/SetContaining.js +++ b/src/core/asymmetric_equality/SetContaining.js @@ -31,8 +31,8 @@ getJasmineRequireObj().SetContaining = function(j$) { return hasAllMatches; }; - SetContaining.prototype.jasmineToString = function() { - return ''; + SetContaining.prototype.jasmineToString = function(pp) { + return ''; }; return SetContaining; diff --git a/src/core/matchers/DiffBuilder.js b/src/core/matchers/DiffBuilder.js index 131b5b92..f5517e17 100644 --- a/src/core/matchers/DiffBuilder.js +++ b/src/core/matchers/DiffBuilder.js @@ -1,12 +1,13 @@ getJasmineRequireObj().DiffBuilder = function(j$) { - return function DiffBuilder() { + return function DiffBuilder(config) { var path = new j$.ObjectPath(), - mismatches = []; + mismatches = [], + prettyPrinter = (config || {}).prettyPrinter || j$.makePrettyPrinter(); return { record: function (actual, expected, formatter) { formatter = formatter || defaultFormatter; - mismatches.push(formatter(actual, expected, path)); + mismatches.push(formatter(actual, expected, path, prettyPrinter)); }, getMessage: function () { @@ -21,12 +22,12 @@ getJasmineRequireObj().DiffBuilder = function(j$) { } }; - function defaultFormatter (actual, expected, path) { + function defaultFormatter (actual, expected, path, prettyPrinter) { return 'Expected ' + path + (path.depth() ? ' = ' : '') + - j$.pp(actual) + + prettyPrinter(actual) + ' to equal ' + - j$.pp(expected) + + prettyPrinter(expected) + '.'; } }; diff --git a/src/core/matchers/matchersUtil.js b/src/core/matchers/matchersUtil.js index 8a0eb75c..f38b9185 100644 --- a/src/core/matchers/matchersUtil.js +++ b/src/core/matchers/matchersUtil.js @@ -45,7 +45,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) { if (i > 0) { message += ','; } - message += ' ' + j$.pp(expected[i]); + message += ' ' + self.pp(expected[i]); } } diff --git a/src/core/matchers/toEqual.js b/src/core/matchers/toEqual.js index d78bde4c..54aea12f 100644 --- a/src/core/matchers/toEqual.js +++ b/src/core/matchers/toEqual.js @@ -14,7 +14,7 @@ getJasmineRequireObj().toEqual = function(j$) { var result = { pass: false }, - diffBuilder = j$.DiffBuilder(); + diffBuilder = j$.DiffBuilder({prettyPrinter: util.pp}); result.pass = util.equals(actual, expected, diffBuilder); diff --git a/src/core/requireInterface.js b/src/core/requireInterface.js index f8f15ac6..52569888 100644 --- a/src/core/requireInterface.js +++ b/src/core/requireInterface.js @@ -316,6 +316,20 @@ getJasmineRequireObj().interface = function(jasmine, env) { return env.addAsyncMatchers(matchers); }; + /** + * Add a custom object formatter for the current scope of specs. + * + * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. + * @name jasmine.addCustomObjectFormatter + * @since 3.6.0 + * @function + * @param {Function} formatter - A function which takes a value to format and returns a string if it knows how to format it, and `undefined` otherwise. + * @see custom_object_formatter + */ + jasmine.addCustomObjectFormatter = function(formatter) { + return env.addCustomObjectFormatter(formatter); + }; + /** * Get the currently booted mock {Clock} for this Jasmine environment. * @name jasmine.clock