diff --git a/.eslintrc.json b/.eslintrc.json index f0b7e80f..29bad671 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,7 +2,7 @@ "env": { "browser": true, "commonjs": true, - "mocha": true, + "jest": true, "node": true }, "extends": "eslint:recommended", diff --git a/.huskyrc.json b/.huskyrc.json index 6731aadb..5ead76e6 100644 --- a/.huskyrc.json +++ b/.huskyrc.json @@ -1,6 +1,6 @@ { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", - "pre-commit": "npm run lint:dts && npm run test:coverage && lint-staged" + "pre-commit": "npm run lint:dts && npm run test:ci && lint-staged" } } diff --git a/.travis.yml b/.travis.yml index 990b41cb..81dfe10a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,11 +8,11 @@ script: - npx commitlint --from=HEAD~1 - npm run lint - npm run lint:dts - - npm run test:coverage + - npm run test:ci - npm run build - npm run benchmark after_success: - - npx nyc report --reporter=text-lcov | npx coveralls + - cat coverage/lcov.info | npx coveralls cache: directories: - node_modules diff --git a/package.json b/package.json index d754bfd4..f7e679a7 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,11 @@ "lint": "eslint --ignore-path .gitignore --ignore-pattern /examples/ .", "lint:dts": "dtslint .", "lint:fix": "npm run lint -- --fix", - "prepublishOnly": "npm run lint && npm run lint:dts && npm test && npm run clean && npm run build", + "prepublishOnly": "npm run lint && npm run lint:dts && npm run test:ci && npm run clean && npm run build", "release": "standard-version --no-verify", - "test": "mocha", - "test:coverage": "nyc npm test", - "test:coverage:report": "nyc report --reporter=html" + "test": "jest --coverage", + "test:ci": "npm test -- --ci", + "test:watch": "npm test -- --watch" }, "repository": { "type": "git", @@ -49,9 +49,8 @@ "eslint": "^7.1.0", "eslint-plugin-prettier": "^3.1.3", "husky": "^4.2.5", + "jest": "^26.0.1", "lint-staged": "^10.2.7", - "mocha": "^7.2.0", - "nyc": "^15.1.0", "preact": "^10.4.4", "prettier": "^2.0.5", "react": "^16", diff --git a/test/__snapshots__/dom-to-react.test.js.snap b/test/__snapshots__/dom-to-react.test.js.snap new file mode 100644 index 00000000..c205788e --- /dev/null +++ b/test/__snapshots__/dom-to-react.test.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DOM to React converts ', + script: '', + style: '', + img: 'Image', + void: '

', + comment: '', + doctype: '', + customElement: + '' +}; + +/** + * SVG. + */ +module.exports.svg = { + simple: 'Inner', + complex: + 'AYour browser does not support inline SVG.' +}; diff --git a/test/helpers/data.json b/test/helpers/data.json deleted file mode 100644 index f49ecdd6..00000000 --- a/test/helpers/data.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "html": { - "single": "

foo

", - "multiple": "

foo

bar

", - "nested": "

foo bar

", - "attributes": "
", - "complex": "Title
Header

Heading


Paragraph

Some text.
", - "textarea": "", - "script": "", - "style": "", - "img": "\"Image\"/", - "void": "

", - "comment": "", - "doctype": "", - "customElement": "" - }, - "svg": { - "simple": "Inner", - "complex": "AYour browser does not support inline SVG." - } -} diff --git a/test/helpers/index.js b/test/helpers/index.js index bed6568d..dd324233 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -1,6 +1,6 @@ -const { isValidElement } = require('react'); const { renderToStaticMarkup } = require('react-dom/server'); -const data = require('./data'); + +module.exports.data = require('./data'); /** * Renders a React element to static HTML markup. @@ -8,14 +8,4 @@ const data = require('./data'); * @param {ReactElement} reactElement - The React element. * @return {String} - The static HTML markup. */ -const render = reactElement => { - if (!isValidElement(reactElement)) { - throw new Error(reactElement, 'is not a valid React element.'); - } - return renderToStaticMarkup(reactElement); -}; - -module.exports = { - render, - data -}; +module.exports.render = reactElement => renderToStaticMarkup(reactElement); diff --git a/test/html-to-react.js b/test/html-to-react.test.js similarity index 70% rename from test/html-to-react.js rename to test/html-to-react.test.js index 3683fd92..7defece0 100644 --- a/test/html-to-react.js +++ b/test/html-to-react.test.js @@ -1,4 +1,3 @@ -const assert = require('assert'); const React = require('react'); const parse = require('..'); const { data, render } = require('./helpers/'); @@ -6,86 +5,94 @@ const { data, render } = require('./helpers/'); describe('HTML to React', () => { describe('exports', () => { it('has default ES Module', () => { - assert.strictEqual(parse.default, parse); + expect(parse.default).toBe(parse); }); it('has domToReact', () => { - assert.strictEqual(parse.domToReact, require('../lib/dom-to-react')); + expect(parse.domToReact).toBe(require('../lib/dom-to-react')); }); it('has htmlToDOM', () => { - assert.strictEqual(parse.htmlToDOM, require('html-dom-parser')); + expect(parse.htmlToDOM).toBe(require('html-dom-parser')); }); }); describe('parser', () => { - [undefined, null, {}, [], 42].forEach(value => { - it(`throws an error if first argument is ${value}`, () => { - assert.throws(() => { - parse(value); - }, TypeError); - }); - }); + it.each([undefined, null, {}, [], 0, 1, () => {}, new Date()])( + 'throws an error if first argument is %p', + input => { + expect(() => { + parse(input); + }).toThrow(TypeError); + } + ); it('converts empty string to empty array', () => { - assert.deepEqual(parse(''), []); + expect(parse('')).toEqual([]); }); it('returns string if it cannot be parsed as HTML', () => { - assert.strictEqual(parse('foo'), 'foo'); + expect(parse('foo')).toBe('foo'); }); it('converts single HTML element to React', () => { const html = data.html.single; const reactElement = parse(html); - assert.strictEqual(render(reactElement), html); + + expect(render(reactElement)).toBe(html); }); it('converts single HTML element and ignores comment', () => { const html = data.html.single; // comment should be ignored const reactElement = parse(html + data.html.comment); - assert.strictEqual(render(reactElement), html); + + expect(render(reactElement)).toBe(html); }); it('converts multiple HTML elements to React', () => { const html = data.html.multiple; const reactElements = parse(html); - assert.strictEqual( - render(React.createElement('div', {}, reactElements)), - '
' + html + '
' - ); + + expect( + render(React.createElement(React.Fragment, {}, reactElements)) + ).toBe(html); }); it('converts complex HTML to React', () => { const html = data.html.complex; const reactElement = parse(data.html.doctype + html); - assert.strictEqual(render(reactElement), html); + + expect(render(reactElement)).toBe(html); }); it('converts empty '; const reactElement = parse(html); - assert.strictEqual(render(reactElement), html); + + expect(render(reactElement)).toBe(html); }); it('converts empty '; const reactElement = parse(html); - assert.strictEqual(render(reactElement), html); + + expect(render(reactElement)).toBe(html); }); it('converts SVG to React', () => { const svg = data.svg.complex; const reactElement = parse(svg); - assert.strictEqual(render(reactElement), svg); + + expect(render(reactElement)).toBe(svg); }); it('decodes HTML entities', () => { const encodedEntities = 'asdf & ÿ ü ''; const decodedEntities = "asdf & ÿ ü '"; const reactElement = parse('' + encodedEntities + ''); - assert.strictEqual(reactElement.props.children, decodedEntities); + + expect(reactElement.props.children).toBe(decodedEntities); }); }); @@ -100,15 +107,15 @@ describe('HTML to React', () => { } } }); - assert.strictEqual( - render(reactElement), + + expect(render(reactElement)).toBe( html.replace('Title', 'Replaced Title') ); }); it('does not override the element if an invalid React element is returned', () => { const html = data.html.complex; - const reactElement = parse(html, { + const options = { replace: node => { if (node.attribs && node.attribs.id === 'header') { return { @@ -117,14 +124,10 @@ describe('HTML to React', () => { }; } } - }); - assert.notEqual( - render(reactElement), - html.replace( - '', - '

Heading

' - ) - ); + }; + const reactElement = parse(html, options); + + expect(render(reactElement)).toBe(html); }); }); @@ -134,7 +137,8 @@ describe('HTML to React', () => { const html = data.html.single; const options = { library: Preact }; const preactElement = parse(html, options); - assert.deepEqual(preactElement, Preact.createElement('p', {}, 'foo')); + + expect(preactElement).toEqual(Preact.createElement('p', {}, 'foo')); }); }); @@ -146,10 +150,8 @@ describe('HTML to React', () => { const html = ''; const options = { htmlparser2: { xmlMode: true } }; const reactElements = parse(html, options); - assert.strictEqual( - render(reactElements), - '' - ); + + expect(render(reactElements)).toBe(''); }); }); @@ -160,7 +162,8 @@ describe('HTML to React', () => { `; const reactElement = parse(html); - assert.strictEqual(render(reactElement), html); + + expect(render(reactElement)).toBe(html); }); it('removes whitespace text nodes when enabled', () => { @@ -168,8 +171,8 @@ describe('HTML to React', () => { text \t\r\n`; const options = { trim: true }; const reactElement = parse(html, options); - assert.strictEqual( - render(reactElement), + + expect(render(reactElement)).toBe( '
text
' ); }); diff --git a/test/types/index.test.tsx b/test/types/index.tsx similarity index 100% rename from test/types/index.test.tsx rename to test/types/index.tsx diff --git a/test/types/lib/dom-to-react.test.tsx b/test/types/lib/dom-to-react.tsx similarity index 100% rename from test/types/lib/dom-to-react.test.tsx rename to test/types/lib/dom-to-react.tsx diff --git a/test/utilities.js b/test/utilities.js deleted file mode 100644 index a1cfbd29..00000000 --- a/test/utilities.js +++ /dev/null @@ -1,130 +0,0 @@ -const assert = require('assert'); -const React = require('react'); -const { - PRESERVE_CUSTOM_ATTRIBUTES, - camelCase, - invertObject, - isCustomComponent -} = require('../lib/utilities'); - -describe('utilities', () => { - describe('camelCase', () => { - [undefined, null, 1337, {}, []].forEach(value => { - it(`throws an error if first argument is ${value}`, () => { - assert.throws(() => { - camelCase(value); - }, TypeError); - }); - }); - - it('does not modify string if it does not need to be camelCased', () => { - [ - ['', ''], - ['foo', 'foo'], - ['fooBar', 'fooBar'], - ['--fooBar', '--fooBar'], - ['--foo-bar', '--foo-bar'], - ['--foo-100', '--foo-100'] - ].forEach(testCase => { - assert.strictEqual(camelCase(testCase[0]), testCase[1]); - }); - }); - - it('camelCases a string', () => { - [ - ['foo-bar', 'fooBar'], - ['foo-bar-baz', 'fooBarBaz'], - ['CAMEL-CASE', 'camelCase'] - ].forEach(testCase => { - assert.strictEqual(camelCase(testCase[0]), testCase[1]); - }); - }); - }); - - describe('invertObject', () => { - [undefined, null, 'foo', 1337].forEach(value => { - it(`throws an error if the first argument is ${value}`, () => { - assert.throws(() => { - invertObject(value); - }, TypeError); - }); - }); - - it('swaps key with value', () => { - assert.deepEqual(invertObject({ foo: 'bar', baz: 'qux' }), { - bar: 'foo', - qux: 'baz' - }); - }); - - it('swaps key with value if value is string', () => { - assert.deepEqual( - invertObject({ - $: 'dollar', - _: 'underscore', - num: 1, - u: undefined, - n: null - }), - { - dollar: '$', - underscore: '_' - } - ); - }); - - describe('options', () => { - it('applies override if provided', () => { - assert.deepEqual( - invertObject({ foo: 'bar', baz: 'qux' }, key => { - if (key === 'foo') { - return ['key', 'value']; - } - }), - { key: 'value', qux: 'baz' } - ); - }); - - it('does not apply override if invalid', () => { - assert.deepEqual( - invertObject({ foo: 'bar', baz: 'qux' }, key => { - if (key === 'foo') { - return ['key']; - } else if (key === 'baz') { - return { key: 'value' }; - } - }), - { bar: 'foo', qux: 'baz' } - ); - }); - }); - }); - - describe('isCustomComponent', () => { - it('returns true if the tag contains a hyphen and is not in the whitelist', () => { - assert.strictEqual(isCustomComponent('my-custom-element'), true); - }); - - it('returns false if the tag is in the whitelist', () => { - assert.strictEqual(isCustomComponent('annotation-xml'), false); - assert.strictEqual(isCustomComponent('color-profile'), false); - assert.strictEqual(isCustomComponent('font-face'), false); - }); - - it('returns true if the props contains an `is` key', () => { - assert.strictEqual( - isCustomComponent('button', { is: 'custom-button' }), - true - ); - }); - }); - - describe('PRESERVE_CUSTOM_ATTRIBUTES', () => { - const isReactGreaterThan15 = - parseInt(React.version.match(/^\d./)[0], 10) >= 16; - - it(`is ${isReactGreaterThan15} when React.version="${React.version}"`, () => { - assert.strictEqual(PRESERVE_CUSTOM_ATTRIBUTES, isReactGreaterThan15); - }); - }); -}); diff --git a/test/utilities.test.js b/test/utilities.test.js new file mode 100644 index 00000000..b8b60923 --- /dev/null +++ b/test/utilities.test.js @@ -0,0 +1,129 @@ +const React = require('react'); +const { + PRESERVE_CUSTOM_ATTRIBUTES, + camelCase, + invertObject, + isCustomComponent +} = require('../lib/utilities'); + +describe('utilities', () => { + describe('camelCase', () => { + it.each([undefined, null, {}, [], 0, 1, () => {}, new Date()])( + 'throws an error if first argument is %p', + input => { + expect(() => { + camelCase(input); + }).toThrow(TypeError); + } + ); + + it.each([ + ['', ''], + ['foo', 'foo'], + ['fooBar', 'fooBar'], + ['--fooBar', '--fooBar'], + ['--foo-bar', '--foo-bar'], + ['--foo-100', '--foo-100'] + ])( + 'does not modify string if it does not need to be camelCased', + (input, expected) => { + expect(camelCase(input)).toBe(expected); + } + ); + + it.each([ + ['foo-bar', 'fooBar'], + ['foo-bar-baz', 'fooBarBaz'], + ['CAMEL-CASE', 'camelCase'] + ])('camelCases a string', (input, expected) => { + expect(camelCase(input)).toBe(expected); + }); + }); + + describe('invertObject', () => { + it.each([undefined, null, 'string', 0, 1, () => {}])( + `throws an error if the first argument is %p`, + input => { + expect(() => { + invertObject(input); + }).toThrow(TypeError); + } + ); + + it('swaps key with value', () => { + expect( + invertObject({ + foo: 'bar', + baz: 'qux' + }) + ).toEqual({ + bar: 'foo', + qux: 'baz' + }); + }); + + it('swaps key with value if value is string', () => { + expect( + invertObject({ + $: 'dollar', + _: 'underscore', + num: 1, + u: undefined, + n: null + }) + ).toEqual({ + dollar: '$', + underscore: '_' + }); + }); + + describe('options', () => { + it('applies override if provided', () => { + expect( + invertObject({ foo: 'bar', baz: 'qux' }, key => { + if (key === 'foo') { + return ['key', 'value']; + } + }) + ).toEqual({ key: 'value', qux: 'baz' }); + }); + + it('does not apply override if invalid', () => { + expect( + invertObject({ foo: 'bar', baz: 'qux' }, key => { + if (key === 'foo') { + return ['key']; + } else if (key === 'baz') { + return { key: 'value' }; + } + }) + ).toEqual({ bar: 'foo', qux: 'baz' }); + }); + }); + }); + + describe('isCustomComponent', () => { + it('returns true if the tag contains a hyphen and is not in the whitelist', () => { + expect(isCustomComponent('my-custom-element')).toBe(true); + }); + + it('returns false if the tag is in the whitelist', () => { + expect(isCustomComponent('annotation-xml')).toBe(false); + expect(isCustomComponent('color-profile')).toBe(false); + expect(isCustomComponent('font-face')).toBe(false); + }); + + it('returns true if the props contains an `is` key', () => { + expect(isCustomComponent('button', { is: 'custom-button' })).toBe(true); + }); + }); + + describe('PRESERVE_CUSTOM_ATTRIBUTES', () => { + const isReactGreaterThan15 = + parseInt(React.version.match(/^\d./)[0], 10) >= 16; + + it(`is ${isReactGreaterThan15} when React.version="${React.version}"`, () => { + expect(PRESERVE_CUSTOM_ATTRIBUTES).toBe(isReactGreaterThan15); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0ae4f4e0..37e9f500 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "files": [ "index.d.ts", "lib/dom-to-react.d.ts", - "test/types/index.test.tsx", - "test/types/lib/dom-to-react.test.tsx" + "test/types/index.tsx", + "test/types/lib/dom-to-react.tsx" ] }