UNPKG

@alexjeffburke/unexpected-react

Version:

Plugin for unexpected, to allow for assertions on the React.js virtual DOM, and the shallow and test renderers

476 lines (397 loc) 21.1 kB
import UnexpectedHtmlLike from 'unexpected-htmllike'; import React from 'react'; const PENDING_TEST_EVENT_TYPE = { dummy: 'Dummy object to identify a pending event on the test renderer' }; function getDefaultOptions(flags) { return { diffWrappers: flags.exactly || flags.withAllWrappers, diffExtraChildren: flags.exactly || flags.withAllChildren, diffExtraAttributes: flags.exactly || flags.withAllAttributes, diffExactClasses: flags.exactly, diffExtraClasses: flags.exactly || flags.withAllClasses }; } /** * * @param options {object} * @param options.ActualAdapter {function} constructor function for the HtmlLike adapter for the `actual` value (usually the renderer) * @param options.ExpectedAdapter {function} constructor function for the HtmlLike adapter for the `expected` value * @param options.QueryAdapter {function} constructor function for the HtmlLike adapter for the query value (`queried for` and `on`) * @param options.actualTypeName {string} name of the unexpected type for the `actual` value * @param options.expectedTypeName {string} name of the unexpected type for the `expected` value * @param options.queryTypeName {string} name of the unexpected type for the query value (used in `queried for` and `on`) * @param options.actualRenderOutputType {string} the unexpected type for the actual output value * @param options.getRenderOutput {function} called with the actual value, and returns the `actualRenderOutputType` type * @param options.getDiffInputFromRenderOutput {function} called with the value from `getRenderOutput`, result passed to HtmlLike diff * @param options.rewrapResult {function} called with the `actual` value (usually the renderer), and the found result * @param options.wrapResultForReturn {function} called with the `actual` value (usually the renderer), and the found result * from HtmlLike `contains()` call (usually the same type returned from `getDiffInputFromRenderOutput`. Used to create a * value that can be passed back to the user as the result of the promise. Used by `queried for` when no further assertion is * provided, therefore the return value is provided as the result of the promise. If this is not present, `rewrapResult` is used. * @param options.triggerEvent {function} called the `actual` value (renderer), the optional target (or null) as the result * from the HtmlLike `contains()` call target, the eventName, and optional eventArgs when provided (undefined otherwise) * @constructor */ function AssertionGenerator(options) { this._options = Object.assign({}, options); this._PENDING_EVENT_IDENTIFIER = (options.mainAssertionGenerator && options.mainAssertionGenerator.getEventIdentifier()) || { dummy: options.actualTypeName + 'PendingEventIdentifier' }; this._actualPendingEventTypeName = options.actualTypeName + 'PendingEvent'; } AssertionGenerator.prototype.getEventIdentifier = function () { return this._PENDING_EVENT_IDENTIFIER; }; AssertionGenerator.prototype.installInto = function installInto(expect) { this._installEqualityAssertions(expect); this._installQueriedFor(expect); this._installPendingEventType(expect); this._installWithEvent(expect); this._installWithEventOn(expect); this._installEventHandlerAssertions(expect); }; AssertionGenerator.prototype.installAlternativeExpected = function (expect) { this._installEqualityAssertions(expect); this._installEventHandlerAssertions(expect); } AssertionGenerator.prototype._installEqualityAssertions = function (expect) { const { actualTypeName, expectedTypeName, getRenderOutput, actualRenderOutputType, getDiffInputFromRenderOutput, ActualAdapter, ExpectedAdapter } = this._options; expect.addAssertion([`<${actualTypeName}> to have [exactly] rendered <${expectedTypeName}>`, `<${actualTypeName}> to have rendered [with all children] [with all wrappers] [with all classes] [with all attributes] <${expectedTypeName}>`], function (expect, subject, renderOutput) { var actual = getRenderOutput(subject); return expect(actual, 'to have [exactly] rendered [with all children] [with all wrappers] [with all classes] [with all attributes]', renderOutput) .then(() => subject); }); expect.addAssertion([ `<${actualRenderOutputType}> to have [exactly] rendered <${expectedTypeName}>`, `<${actualRenderOutputType}> to have rendered [with all children] [with all wrappers] [with all classes] [with all attributes] <${expectedTypeName}>` ], function (expect, subject, renderOutput) { const exactly = this.flags.exactly; const withAllChildren = this.flags['with all children']; const withAllWrappers = this.flags['with all wrappers']; const withAllClasses = this.flags['with all classes']; const withAllAttributes = this.flags['with all attributes']; const actualAdapter = new ActualAdapter(); const expectedAdapter = new ExpectedAdapter(); const testHtmlLike = new UnexpectedHtmlLike(actualAdapter); if (!exactly) { expectedAdapter.setOptions({concatTextContent: true}); actualAdapter.setOptions({concatTextContent: true}); } const options = getDefaultOptions({exactly, withAllWrappers, withAllChildren, withAllClasses, withAllAttributes}); const diffResult = testHtmlLike.diff(expectedAdapter, getDiffInputFromRenderOutput(subject), renderOutput, expect, options); return testHtmlLike.withResult(diffResult, result => { if (result.weight !== 0) { return expect.fail({ diff: function (output, diff, inspect) { return { diff: output.append(testHtmlLike.render(result, output.clone(), diff, inspect)) }; } }); } return result; }); }); expect.addAssertion([`<${actualTypeName}> [not] to contain [exactly] <${expectedTypeName}|string>`, `<${actualTypeName}> [not] to contain [with all children] [with all wrappers] [with all classes] [with all attributes] <${expectedTypeName}|string>`], function (expect, subject, renderOutput) { var actual = getRenderOutput(subject); return expect(actual, '[not] to contain [exactly] [with all children] [with all wrappers] [with all classes] [with all attributes]', renderOutput); }); expect.addAssertion([`<${actualRenderOutputType}> [not] to contain [exactly] <${expectedTypeName}|string>`, `<${actualRenderOutputType}> [not] to contain [with all children] [with all wrappers] [with all classes] [with all attributes] <${expectedTypeName}|string>`], function (expect, subject, expected) { var not = this.flags.not; var exactly = this.flags.exactly; var withAllChildren = this.flags['with all children']; var withAllWrappers = this.flags['with all wrappers']; var withAllClasses = this.flags['with all classes']; var withAllAttributes = this.flags['with all attributes']; var actualAdapter = new ActualAdapter(); var expectedAdapter = new ExpectedAdapter(); var testHtmlLike = new UnexpectedHtmlLike(actualAdapter); if (!exactly) { actualAdapter.setOptions({concatTextContent: true}); expectedAdapter.setOptions({concatTextContent: true}); } var options = getDefaultOptions({exactly, withAllWrappers, withAllChildren, withAllClasses, withAllAttributes}); const containsResult = testHtmlLike.contains(expectedAdapter, getDiffInputFromRenderOutput(subject), expected, expect, options); return testHtmlLike.withResult(containsResult, result => { if (not) { if (result.found) { expect.fail({ diff: (output, diff, inspect) => { return { diff: output.error('but found the following match').nl().append(testHtmlLike.render(result.bestMatch, output.clone(), diff, inspect)) }; } }); } return; } if (!result.found) { expect.fail({ diff: function (output, diff, inspect) { return { diff: output.error('the best match was').nl().append(testHtmlLike.render(result.bestMatch, output.clone(), diff, inspect)) }; } }); } }); }); // More generic assertions expect.addAssertion(`<${actualTypeName}> to equal <${expectedTypeName}>`, function (expect, subject, expected) { expect(getRenderOutput(subject), 'to equal', expected); }); expect.addAssertion(`<${actualRenderOutputType}> to equal <${expectedTypeName}>`, function (expect, subject, expected) { expect(subject, 'to have exactly rendered', expected); }); expect.addAssertion(`<${actualTypeName}> to satisfy <${expectedTypeName}>`, function (expect, subject, expected) { expect(getRenderOutput(subject), 'to satisfy', expected); }); expect.addAssertion(`<${actualRenderOutputType}> to satisfy <${expectedTypeName}>`, function (expect, subject, expected) { expect(subject, 'to have rendered', expected); }); }; AssertionGenerator.prototype._installQueriedFor = function (expect) { const { actualTypeName, queryTypeName, getRenderOutput, actualRenderOutputType, getDiffInputFromRenderOutput, rewrapResult, wrapResultForReturn, ActualAdapter, QueryAdapter } = this._options; expect.addAssertion([`<${actualTypeName}> queried for [exactly] <${queryTypeName}> <assertion?>`, `<${actualTypeName}> queried for [with all children] [with all wrapppers] [with all classes] [with all attributes] <${queryTypeName}> <assertion?>` ], function (expect, subject, query, assertion) { return expect.apply(expect, [ getRenderOutput(subject), 'queried for [exactly] [with all children] [with all wrappers] [with all classes] [with all attributes]', query ].concat(Array.prototype.slice.call(arguments, 3))); }); expect.addAssertion([`<${actualRenderOutputType}> queried for [exactly] <${queryTypeName}> <assertion?>`, `<${actualRenderOutputType}> queried for [with all children] [with all wrapppers] [with all classes] [with all attributes] <${queryTypeName}> <assertion?>`], function (expect, subject, query) { var exactly = this.flags.exactly; var withAllChildren = this.flags['with all children']; var withAllWrappers = this.flags['with all wrappers']; var withAllClasses = this.flags['with all classes']; var withAllAttributes = this.flags['with all attributes']; var actualAdapter = new ActualAdapter(); var queryAdapter = new QueryAdapter(); var testHtmlLike = new UnexpectedHtmlLike(actualAdapter); if (!exactly) { actualAdapter.setOptions({concatTextContent: true}); queryAdapter.setOptions({concatTextContent: true}); } const options = getDefaultOptions({exactly, withAllWrappers, withAllChildren, withAllClasses, withAllAttributes}); options.findTargetAttrib = 'queryTarget'; const containsResult = testHtmlLike.contains(queryAdapter, getDiffInputFromRenderOutput(subject), query, expect, options); const args = arguments; return testHtmlLike.withResult(containsResult, function (result) { if (!result.found) { expect.fail({ diff: (output, diff, inspect) => { const resultOutput = { diff: output.error('`queried for` found no match.') }; if (result.bestMatch) { resultOutput.diff.error(' The best match was') .nl() .append(testHtmlLike.render(result.bestMatch, output.clone(), diff, inspect)); } return resultOutput; } }); } if (args.length > 3) { // There is an assertion continuation... expect.errorMode = 'nested' const s = rewrapResult(subject, result.bestMatch.target || result.bestMatchItem); return expect.apply(null, [ rewrapResult(subject, result.bestMatch.target || result.bestMatchItem) ].concat(Array.prototype.slice.call(args, 3))) return expect.shift(rewrapResult(subject, result.bestMatch.target || result.bestMatchItem)); } // There is no assertion continuation, so we need to wrap the result for public consumption // i.e. create a value that we can give back from the `expect` promise return expect.shift((wrapResultForReturn || rewrapResult)(subject, result.bestMatch.target || result.bestMatchItem)); }); }); }; AssertionGenerator.prototype._installPendingEventType = function (expect) { const actualPendingEventTypeName = this._actualPendingEventTypeName; const PENDING_EVENT_IDENTIFIER = this._PENDING_EVENT_IDENTIFIER; expect.addType({ name: actualPendingEventTypeName, base: 'object', identify(value) { return value && typeof value === 'object' && value.$$typeof === PENDING_EVENT_IDENTIFIER; }, inspect(value, depth, output, inspect) { return output.append(inspect(value.renderer)).red(' with pending event \'').cyan(value.eventName).red('\''); } }); }; AssertionGenerator.prototype._installWithEvent = function (expect) { const { actualTypeName, actualRenderOutputType, triggerEvent, canTriggerEventsOnOutputType } = this._options; let { wrapResultForReturn = (value) => value } = this._options; const actualPendingEventTypeName = this._actualPendingEventTypeName; const PENDING_EVENT_IDENTIFIER = this._PENDING_EVENT_IDENTIFIER; expect.addAssertion(`<${actualTypeName}> with event <string> <assertion?>`, function (expect, subject, eventName, ...assertion) { if (arguments.length > 3) { return expect.apply(null, [{ $$typeof: PENDING_EVENT_IDENTIFIER, renderer: subject, eventName: eventName }].concat(assertion)); } else { triggerEvent(subject, null, eventName); return expect.shift(wrapResultForReturn(subject)); } }); expect.addAssertion(`<${actualTypeName}> with event <string> <object> <assertion?>`, function (expect, subject, eventName, args) { if (arguments.length > 4) { return expect.shift({ $$typeof: PENDING_EVENT_IDENTIFIER, renderer: subject, eventName: eventName, eventArgs: args }); } else { triggerEvent(subject, null, eventName, args); return expect.shift(subject); } }); if (canTriggerEventsOnOutputType) { expect.addAssertion(`<${actualRenderOutputType}> with event <string> <assertion?>`, function (expect, subject, eventName, ...assertion) { if (arguments.length > 3) { return expect.apply(null, [{ $$typeof: PENDING_EVENT_IDENTIFIER, renderer: subject, eventName: eventName, isOutputType: true }].concat(assertion)); } else { triggerEvent(subject, null, eventName); return expect.shift(wrapResultForReturn(subject)); } }); expect.addAssertion(`<${actualRenderOutputType}> with event <string> <object> <assertion?>`, function (expect, subject, eventName, args) { if (arguments.length > 4) { return expect.shift({ $$typeof: PENDING_EVENT_IDENTIFIER, renderer: subject, eventName: eventName, eventArgs: args, isOutputType: true }); } else { triggerEvent(subject, null, eventName, args); return expect.shift(subject); } }); } expect.addAssertion(`<${actualPendingEventTypeName}> [and] with event <string> <assertion?>`, function (expect, subject, eventName) { triggerEvent(subject.renderer, subject.target, subject.eventName, subject.eventArgs); if (arguments.length > 3) { return expect.shift({ $$typeof: PENDING_EVENT_IDENTIFIER, renderer: subject.renderer, eventName: eventName }); } else { triggerEvent(subject.renderer, null, eventName); return expect.shift(subject.renderer); } }); expect.addAssertion(`<${actualPendingEventTypeName}> [and] with event <string> <object> <assertion?>`, function (expect, subject, eventName, eventArgs) { triggerEvent(subject.renderer, subject.target, subject.eventName, subject.eventArgs); if (arguments.length > 4) { return expect.shift({ $$typeof: PENDING_EVENT_IDENTIFIER, renderer: subject.renderer, eventName: eventName, eventArgs: eventArgs }); } else { triggerEvent(subject.renderer, null, eventName, eventArgs); return expect.shift(subject.renderer); } }); }; AssertionGenerator.prototype._installWithEventOn = function (expect) { const { actualTypeName, queryTypeName, expectedTypeName, getRenderOutput, getDiffInputFromRenderOutput, triggerEvent, ActualAdapter, QueryAdapter } = this._options; const actualPendingEventTypeName = this._actualPendingEventTypeName; expect.addAssertion(`<${actualPendingEventTypeName}> on [exactly] [with all children] [with all wrappers] [with all classes] [with all attributes]<${queryTypeName}> <assertion?>`, function (expect, subject, target) { const actualAdapter = new ActualAdapter({ convertToString: true, concatTextContent: true }); const queryAdapter = new QueryAdapter({ convertToString: true, concatTextContent: true }); const testHtmlLike = new UnexpectedHtmlLike(actualAdapter); const exactly = this.flags.exactly; const withAllChildren = this.flags['with all children']; const withAllWrappers = this.flags['with all wrappers']; const withAllClasses = this.flags['with all classes']; const withAllAttributes = this.flags['with all attributes']; const options = getDefaultOptions({ exactly, withAllWrappers, withAllChildren, withAllClasses, withAllAttributes}); options.findTargetAttrib = 'eventTarget'; const containsResult = testHtmlLike.contains(queryAdapter, getDiffInputFromRenderOutput(getRenderOutput(subject.renderer)), target, expect, options); return testHtmlLike.withResult(containsResult, result => { if (!result.found) { return expect.fail({ diff: function (output, diff, inspect) { output.error('Could not find the target for the event. '); if (result.bestMatch) { output.error('The best match was').nl().nl().append(testHtmlLike.render(result.bestMatch, output.clone(), diff, inspect)); } return output; } }); } const newSubject = Object.assign({}, subject, { target: result.bestMatch.target || result.bestMatchItem }); if (arguments.length > 3) { return expect.shift(newSubject); } else { triggerEvent(newSubject.renderer, newSubject.target, newSubject.eventName, newSubject.eventArgs); return expect.shift(newSubject.renderer); } }); }); expect.addAssertion([`<${actualPendingEventTypeName}> queried for [exactly] <${queryTypeName}> <assertion?>`, `<${actualPendingEventTypeName}> queried for [with all children] [with all wrappers] [with all classes] [with all attributes] <${queryTypeName}> <assertion?>`], function (expect, subject, expected) { triggerEvent(subject.renderer, subject.target, subject.eventName, subject.eventArgs); return expect.apply(expect, [subject.renderer, 'queried for [exactly] [with all children] [with all wrappers] [with all classes] [with all attributes]', expected] .concat(Array.prototype.slice.call(arguments, 3))); } ); }; AssertionGenerator.prototype._installEventHandlerAssertions = function (expect) { const { actualTypeName, expectedTypeName, triggerEvent } = this._options; const actualPendingEventTypeName = this._actualPendingEventTypeName; expect.addAssertion([`<${actualPendingEventTypeName}> [not] to contain [exactly] <${expectedTypeName}>`, `<${actualPendingEventTypeName}> [not] to contain [with all children] [with all wrappers] [with all classes] [with all attributes] <${expectedTypeName}>`], function (expect, subject, expected) { triggerEvent(subject.renderer, subject.target, subject.eventName, subject.eventArgs); return expect(subject.renderer, '[not] to contain [exactly] [with all children] [with all wrappers] [with all classes] [with all attributes]', expected); }); expect.addAssertion(`<${actualPendingEventTypeName}> to have [exactly] rendered [with all children] [with all wrappers] [with all classes] [with all attributes] <${expectedTypeName}>`, function (expect, subject, expected) { triggerEvent(subject.renderer, subject.target, subject.eventName, subject.eventArgs); return expect(subject.renderer, 'to have [exactly] rendered [with all children] [with all wrappers] [with all classes] [with all attributes]', expected); }); }; export default AssertionGenerator;