From 7f68960dc950c3435990e669ac95f317c00d20c6 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 17 Apr 2017 16:57:49 -0700 Subject: [PATCH] Adds HtmlElement pretty-format plugin. (#3230) * Adds HtmlElement pretty-format plugin. * Adds missing copyright and 'use strict' * Fixes variable naming / formatting. Reworks short circuiting logic for `isHTMLElement` function. * Improves performance for isHtmlElement. * Updating snapshots * Revert "Updating snapshots" This reverts commit 55f797f58ab2915f8fed5178fdff3bd1562e5d1c. * Fixes node 4 syntax * - Switches to tagName rather than constructor. * Adds HTMLElement Plugin usage. * Removes `HTMLElement` fallback in favor of tagName --- packages/jest-diff/src/index.js | 2 + packages/jest-matcher-utils/src/index.js | 9 +- .../src/__tests__/plugins-test.js | 2 +- packages/jest-snapshot/src/plugins.js | 9 +- .../src/__tests__/HTMLElementPlugin-test.js | 94 ++++++++++++++++ .../src/__tests__/ImmutablePlugins-test.js | 45 +------- .../src/__tests__/expect-util.js | 52 +++++++++ .../pretty-format/src/plugins/HTMLElement.js | 102 ++++++++++++++++++ 8 files changed, 267 insertions(+), 48 deletions(-) create mode 100644 packages/pretty-format/src/__tests__/HTMLElementPlugin-test.js create mode 100644 packages/pretty-format/src/__tests__/expect-util.js create mode 100644 packages/pretty-format/src/plugins/HTMLElement.js diff --git a/packages/jest-diff/src/index.js b/packages/jest-diff/src/index.js index f54fc1c5607b..57564dd3434d 100644 --- a/packages/jest-diff/src/index.js +++ b/packages/jest-diff/src/index.js @@ -15,6 +15,7 @@ import type {DiffOptions} from './diffStrings'; const ReactElementPlugin = require('pretty-format/build/plugins/ReactElement'); const ReactTestComponentPlugin = require('pretty-format/build/plugins/ReactTestComponent'); const AsymmetricMatcherPlugin = require('pretty-format/build/plugins/AsymmetricMatcher'); +const HTMLElementPlugin = require('pretty-format/build/plugins/HTMLElement'); const ImmutablePlugins = require('pretty-format/build/plugins/ImmutablePlugins'); const chalk = require('chalk'); @@ -31,6 +32,7 @@ const PLUGINS = [ ReactTestComponentPlugin, ReactElementPlugin, AsymmetricMatcherPlugin, + HTMLElementPlugin, ].concat(ImmutablePlugins); const FORMAT_OPTIONS = { plugins: PLUGINS, diff --git a/packages/jest-matcher-utils/src/index.js b/packages/jest-matcher-utils/src/index.js index d8043e256441..b43b384b7dea 100644 --- a/packages/jest-matcher-utils/src/index.js +++ b/packages/jest-matcher-utils/src/index.js @@ -14,11 +14,14 @@ const chalk = require('chalk'); const prettyFormat = require('pretty-format'); const AsymmetricMatcherPlugin = require('pretty-format/build/plugins/AsymmetricMatcher'); const ReactElementPlugin = require('pretty-format/build/plugins/ReactElement'); +const HTMLElementPlugin = require('pretty-format/build/plugins/HTMLElement'); const ImmutablePlugins = require('pretty-format/build/plugins/ImmutablePlugins'); -const PLUGINS = [AsymmetricMatcherPlugin, ReactElementPlugin].concat( - ImmutablePlugins, -); +const PLUGINS = [ + AsymmetricMatcherPlugin, + ReactElementPlugin, + HTMLElementPlugin, +].concat(ImmutablePlugins); export type ValueType = | 'array' diff --git a/packages/jest-snapshot/src/__tests__/plugins-test.js b/packages/jest-snapshot/src/__tests__/plugins-test.js index 90dfefce45ba..36b956c924a7 100644 --- a/packages/jest-snapshot/src/__tests__/plugins-test.js +++ b/packages/jest-snapshot/src/__tests__/plugins-test.js @@ -28,7 +28,7 @@ const testPath = names => { it('gets plugins', () => { const {getSerializers} = require('../plugins'); const plugins = getSerializers(); - expect(plugins.length).toBe(8); + expect(plugins.length).toBe(9); }); it('adds plugins from an empty array', () => testPath([])); diff --git a/packages/jest-snapshot/src/plugins.js b/packages/jest-snapshot/src/plugins.js index b83616e7b8ff..252cd82dc02b 100644 --- a/packages/jest-snapshot/src/plugins.js +++ b/packages/jest-snapshot/src/plugins.js @@ -11,11 +11,14 @@ const ReactElementPlugin = require('pretty-format/build/plugins/ReactElement'); const ReactTestComponentPlugin = require('pretty-format/build/plugins/ReactTestComponent'); +const HTMLElementPlugin = require('pretty-format/build/plugins/HTMLElement'); const ImmutablePlugins = require('pretty-format/build/plugins/ImmutablePlugins'); -let PLUGINS = [ReactElementPlugin, ReactTestComponentPlugin].concat( - ImmutablePlugins, -); +let PLUGINS = [ + ReactElementPlugin, + ReactTestComponentPlugin, + HTMLElementPlugin, +].concat(ImmutablePlugins); // Prepend to list so the last added is the first tested. exports.addSerializer = (plugin: any) => { diff --git a/packages/pretty-format/src/__tests__/HTMLElementPlugin-test.js b/packages/pretty-format/src/__tests__/HTMLElementPlugin-test.js new file mode 100644 index 000000000000..1227891f9773 --- /dev/null +++ b/packages/pretty-format/src/__tests__/HTMLElementPlugin-test.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2014-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-environment jsdom + */ +/* eslint-disable max-len */ +/* eslint-env browser*/ + +'use strict'; + +const HTMLElementPlugin = require('../plugins/HTMLElement'); +const toPrettyPrintTo = require('./expect-util').getPrettyPrint([ + HTMLElementPlugin, +]); + +expect.extend({toPrettyPrintTo}); + +describe('HTMLElement Plugin', () => { + it('supports a single HTML element', () => { + expect(document.createElement('div')).toPrettyPrintTo('
'); + }); + + it('supports an HTML element with a class property', () => { + const parent = document.createElement('div'); + parent.className = 'classy'; + + expect(parent).toPrettyPrintTo(''); + }); + + it('supports an HTML element with a title property', () => { + const parent = document.createElement('div'); + parent.title = 'title text'; + + expect(parent).toPrettyPrintTo(''); + }); + + it('supports an HTML element with a single attribute', () => { + const parent = document.createElement('div'); + parent.setAttribute('class', 'classy'); + + expect(parent).toPrettyPrintTo(''); + }); + + it('supports an HTML element with multiple attributes', () => { + const parent = document.createElement('div'); + parent.setAttribute('id', 123); + parent.setAttribute('class', 'classy'); + + expect(parent).toPrettyPrintTo('', { + }); + }); + + it('supports an element with text content', () => { + const parent = document.createElement('div'); + parent.innerHTML = 'texty texty'; + + expect(parent).toPrettyPrintTo('
\n texty texty\n
'); + }); + + it('supports nested elements', () => { + const parent = document.createElement('div'); + const child = document.createElement('span'); + parent.appendChild(child); + expect(parent).toPrettyPrintTo('
\n \n
'); + }); + + it('supports nested elements with attributes', () => { + const parent = document.createElement('div'); + const child = document.createElement('span'); + parent.appendChild(child); + + child.setAttribute('id', 123); + child.setAttribute('class', 'classy'); + + expect(parent).toPrettyPrintTo( + '
\n \n
', + ); + }); + + it('supports nested elements with text content', () => { + const parent = document.createElement('div'); + const child = document.createElement('span'); + parent.appendChild(child); + child.textContent = 'texty texty'; + + expect(parent).toPrettyPrintTo( + '
\n \n texty texty\n \n
', + ); + }); +}); diff --git a/packages/pretty-format/src/__tests__/ImmutablePlugins-test.js b/packages/pretty-format/src/__tests__/ImmutablePlugins-test.js index b1b7ec0a892c..d6690de2cfad 100644 --- a/packages/pretty-format/src/__tests__/ImmutablePlugins-test.js +++ b/packages/pretty-format/src/__tests__/ImmutablePlugins-test.js @@ -10,52 +10,15 @@ 'use strict'; const React = require('react'); -const diff = require('jest-diff'); -const prettyFormat = require('../'); const Immutable = require('immutable'); const ReactElementPlugin = require('../plugins/ReactElement'); const ReactTestComponentPlugin = require('../plugins/ReactTestComponent'); const ImmutablePlugins = require('../plugins/ImmutablePlugins'); +const toPrettyPrintTo = require('./expect-util').getPrettyPrint( + [ReactElementPlugin, ReactTestComponentPlugin].concat(ImmutablePlugins), +); -expect.extend({ - toPrettyPrintTo(received, expected, opts) { - const prettyPrintImmutable = prettyFormat( - received, - Object.assign( - { - plugins: [ReactElementPlugin, ReactTestComponentPlugin].concat( - ImmutablePlugins, - ), - }, - opts, - ), - ); - const pass = prettyPrintImmutable === expected; - - const message = pass - ? () => - this.utils.matcherHint('.not.toBe') + - '\n\n' + - `Expected value to not be:\n` + - ` ${this.utils.printExpected(expected)}\n` + - `Received:\n` + - ` ${this.utils.printReceived(prettyPrintImmutable)}` - : () => { - const diffString = diff(expected, prettyPrintImmutable, { - expand: this.expand, - }); - return this.utils.matcherHint('.toBe') + - '\n\n' + - `Expected value to be:\n` + - ` ${this.utils.printExpected(expected)}\n` + - `Received:\n` + - ` ${this.utils.printReceived(prettyPrintImmutable)}` + - (diffString ? `\n\nDifference:\n\n${diffString}` : ''); - }; - - return {actual: prettyPrintImmutable, message, pass}; - }, -}); +expect.extend({toPrettyPrintTo}); describe('Immutable.OrderedSet plugin', () => { it('supports an empty set', () => { diff --git a/packages/pretty-format/src/__tests__/expect-util.js b/packages/pretty-format/src/__tests__/expect-util.js new file mode 100644 index 000000000000..2be6fbdaf216 --- /dev/null +++ b/packages/pretty-format/src/__tests__/expect-util.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2014-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'; + +const diff = require('jest-diff'); +const prettyFormat = require('../'); + +module.exports = { + getPrettyPrint: plugins => + (received, expected, opts) => { + const prettyFormatted = prettyFormat( + received, + Object.assign( + { + plugins, + }, + opts, + ), + ); + const pass = prettyFormatted === expected; + + const message = pass + ? () => + this.utils.matcherHint('.not.toBe') + + '\n\n' + + `Expected value to not be:\n` + + ` ${this.utils.printExpected(expected)}\n` + + `Received:\n` + + ` ${this.utils.printReceived(prettyFormatted)}` + : () => { + const diffString = diff(expected, prettyFormatted, { + expand: this.expand, + }); + return this.utils.matcherHint('.toBe') + + '\n\n' + + `Expected value to be:\n` + + ` ${this.utils.printExpected(expected)}\n` + + `Received:\n` + + ` ${this.utils.printReceived(prettyFormatted)}` + + (diffString ? `\n\nDifference:\n\n${diffString}` : ''); + }; + + return {actual: prettyFormatted, message, pass}; + }, +}; diff --git a/packages/pretty-format/src/plugins/HTMLElement.js b/packages/pretty-format/src/plugins/HTMLElement.js new file mode 100644 index 000000000000..710268562278 --- /dev/null +++ b/packages/pretty-format/src/plugins/HTMLElement.js @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2014-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. + * + * @flow + */ + +'use strict'; + +import type {Colors, Indent, Options, Print, Plugin} from '../types.js'; + +const escapeHTML = require('./lib/escapeHTML'); +const HTML_ELEMENT_REGEXP = /(HTML\w*?Element)/; +const test = isHTMLElement; + +function isHTMLElement(value: any) { + return value !== undefined && + value !== null && + value.nodeType === 1 && + value.constructor !== undefined && + value.constructor.name !== undefined && + HTML_ELEMENT_REGEXP.test(value.constructor.name); +} + +function printChildren(flatChildren, print, indent, colors, opts) { + return flatChildren + .map(node => { + if (typeof node === 'object') { + return print(node, print, indent, colors, opts); + } else if (typeof node === 'string') { + return colors.content.open + escapeHTML(node) + colors.content.close; + } else { + return print(node); + } + }) + .join(opts.edgeSpacing); +} + +function printAttributes(attributes, print, indent, colors, opts) { + return attributes + .sort() + .map(attribute => { + return opts.spacing + + indent(colors.prop.open + attribute.name + colors.prop.close + '=') + + colors.value.open + + `"${attribute.value}"` + + colors.value.close; + }) + .join(''); +} + +const print = ( + element: any, + print: Print, + indent: Indent, + opts: Options, + colors: Colors, +) => { + let result = colors.tag.open + '<'; + const elementName = element.tagName.toLowerCase(); + result += elementName + colors.tag.close; + + const hasAttributes = element.attributes && element.attributes.length; + if (hasAttributes) { + const attributes = Array.prototype.slice.call(element.attributes); + result += printAttributes(attributes, print, indent, colors, opts); + } + + const flatChildren = Array.prototype.slice.call(element.children); + if (!flatChildren.length && element.textContent) { + flatChildren.push(element.textContent); + } + + const closeInNewLine = hasAttributes && !opts.min; + if (flatChildren.length) { + const children = printChildren(flatChildren, print, indent, colors, opts); + result += colors.tag.open + + (closeInNewLine ? '\n' : '') + + '>' + + colors.tag.close + + opts.edgeSpacing + + indent(children) + + opts.edgeSpacing + + colors.tag.open + + '' + + colors.tag.close; + } else { + result += colors.tag.open + + (closeInNewLine ? '\n' : ' ') + + '/>' + + colors.tag.close; + } + + return result; +}; + +module.exports = ({print, test}: Plugin);