From 4b0e342016662122d385db74a30702850b7a7251 Mon Sep 17 00:00:00 2001 From: Alan Pierce Date: Tue, 16 Aug 2022 11:34:49 -0700 Subject: [PATCH] Refactor jsx-test to better support testing the automatic runtime Since the automatic runtime effectively doubles the number of cases needing test coverage, plus has a number of cases within itself, this PR refactors the JSX tests to make it easy to declare the result for multiple configurations in the same test. This should make it smoother to write the automatic case for each test where it feels relevant, plus makes it easier to test ESM vs CJS and dev vs prod for the same syntax case. --- test/jsx-test.ts | 401 ++++++++++++++++++++++++++++------------------- test/util.ts | 9 +- 2 files changed, 242 insertions(+), 168 deletions(-) diff --git a/test/jsx-test.ts b/test/jsx-test.ts index 0bca0173..c53d9389 100644 --- a/test/jsx-test.ts +++ b/test/jsx-test.ts @@ -1,167 +1,231 @@ import {throws} from "assert"; -import {transform, Transform} from "../src"; +import {transform, Options} from "../src"; import {IMPORT_DEFAULT_PREFIX, JSX_PREFIX} from "./prefixes"; import * as util from "./util"; const {devProps} = util; -function assertResult( +interface JSXExpectations { + expectedClassicDevESMResult?: string; + expectedClassicProdESMResult?: string; + expectedClassicDevCJSResult?: string; + expectedClassicProdCJSResult?: string; +} + +const optionsForMode: {[k in keyof JSXExpectations]-?: Options} = { + expectedClassicDevESMResult: {transforms: ["jsx"], production: false}, + expectedClassicProdESMResult: {transforms: ["jsx"], production: true}, + expectedClassicDevCJSResult: {transforms: ["jsx", "imports"], production: false}, + expectedClassicProdCJSResult: {transforms: ["jsx", "imports"], production: true}, +}; + +function assertJSXResult( code: string, - expectedResult: string, - { - extraTransforms, - jsxPragma, - jsxFragmentPragma, - production, - }: { - extraTransforms?: Array; - jsxPragma?: string; - jsxFragmentPragma?: string; - production?: boolean; - } = {}, + jsxExpectations: JSXExpectations, + options: Partial = {}, ): void { - const transforms: Array = ["jsx", ...(extraTransforms || [])]; - util.assertResult(code, expectedResult, {transforms, jsxPragma, jsxFragmentPragma, production}); + for (const [expectationName, expectedResult] of Object.entries(jsxExpectations)) { + util.assertResult( + code, + expectedResult, + {...optionsForMode[expectationName as keyof JSXExpectations], ...options}, + `${expectationName} did not match`, + ); + } } describe("transform JSX", () => { it("transforms a self-closing JSX element", () => { - assertResult( + assertJSXResult( ` `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(Foo, {${devProps(2)}} ) `, + expectedClassicProdESMResult: ` + React.createElement(Foo, null ) + `, + }, + ); + }); + + it("handles a JSX element with a key, multiple props, and multiple children", () => { + assertJSXResult( + ` +
+ Hello world + ! +
+ `, + { + expectedClassicDevESMResult: `${JSX_PREFIX} + React.createElement('div', { a: "foo", key: "some-key", c: "test", ${devProps( + 2, + )}}, "Hello world" + + , React.createElement('span', { className: "some-class", ${devProps(4)}}, "!") + ) + `, + expectedClassicProdESMResult: ` + React.createElement('div', { a: "foo", key: "some-key", c: "test",}, "Hello world" + + , React.createElement('span', { className: "some-class",}, "!") + ) + `, + }, ); }); it("handles more esoteric component names", () => { - assertResult( + assertJSXResult( ` <_Foo /> `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(_Foo, {${devProps(2)}} ) `, + }, ); - assertResult( + assertJSXResult( ` <$ /> `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement($, {${devProps(2)}} ) `, + }, ); - assertResult( + assertJSXResult( ` <é /> `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(é, {${devProps(2)}} ) `, + }, ); }); it("transforms nested JSX elements", () => { - assertResult( + assertJSXResult( `
`, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('div', {${devProps(2)}}, React.createElement('span', {${devProps(2)}})) `, + }, ); }); it("transforms interpolated children", () => { - assertResult( + assertJSXResult( `
{x}
`, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('div', {${devProps(2)}}, x) `, + }, ); }); it("handles string property values", () => { - assertResult( + assertJSXResult( ` `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(A, { foo: "bar", ${devProps(2)}} ) `, + }, ); }); it("handles element property values", () => { - assertResult( + assertJSXResult( ` /> `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(A, { foo: React.createElement(B, {${devProps(2)}} ), ${devProps(2)}} ) `, + }, ); }); it("handles fragment property values", () => { - assertResult( + assertJSXResult( ` Hi /> `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(A, { foo: React.createElement(React.Fragment, null, "Hi"), ${devProps( 2, )}} ) `, + }, ); }); it("handles property keys that require quoting", () => { - assertResult( + assertJSXResult( ` `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(A, { 'foo-bar': true, ${devProps(2)}} ) `, + }, ); }); it("handles shorthand property keys that require quoting", () => { - assertResult( + assertJSXResult( ` `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(A, { 'foo-bar': true, ${devProps(2)}} ) `, + }, ); }); it("handles inline comments", () => { - assertResult( + assertJSXResult( ` `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(A, { b: "c", // A comment d: "e", ${devProps(2)}} /* Another comment */ ) `, + }, ); }); it("handles multiline strings", () => { - assertResult( + assertJSXResult( ` const x = (
@@ -170,7 +234,8 @@ describe("transform JSX", () => {
); `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const x = ( React.createElement('div', {${devProps(3)}}, "foo bar baz" @@ -178,11 +243,12 @@ describe("transform JSX", () => { ) ); `, + }, ); }); it("handles nested JSX tags", () => { - assertResult( + assertJSXResult( ` const x = (
@@ -190,134 +256,159 @@ describe("transform JSX", () => {
); `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const x = ( React.createElement('div', {${devProps(3)}} , React.createElement(Span, {${devProps(4)}} ) ) ); `, + }, ); }); it("handles complex lower-case tag values", () => { - assertResult( + assertJSXResult( ` `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement(a.b, { c: "d", ${devProps(2)}} ) `, + }, ); }); it("handles prop spread operators", () => { - assertResult( + assertJSXResult( `
`, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('a', { ...b, c: "d", ${devProps(2)}} ) `, + expectedClassicProdESMResult: ` + React.createElement('a', { ...b, c: "d",} ) + `, + }, ); }); it("handles HTML entities", () => { - assertResult( + assertJSXResult( ` a>b `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('span', {${devProps(2)}}, "a>b") `, + }, ); }); it("handles hex unicode HTML entities", () => { - assertResult( + assertJSXResult( ` a>b `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('span', {${devProps(2)}}, "a>b") `, + }, ); }); it("handles decimal unicode HTML entities", () => { - assertResult( + assertJSXResult( ` a>b `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('span', {${devProps(2)}}, "a>b") `, + }, ); }); it("does not transform empty number HTML entities", () => { - assertResult( + assertJSXResult( ` a&#;b `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('span', {${devProps(2)}}, "a&#;b") `, + }, ); }); it("allows adjacent HTML entities", () => { - assertResult( + assertJSXResult( ` addb `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('span', {${devProps(2)}}, "addb") `, + }, ); }); it("allows HTML entity right after near-entity with semicolon missing", () => { - assertResult( + assertJSXResult( ` addb `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('span', {${devProps(2)}}, "addb") `, + }, ); }); it("handles ampersand in HTML", () => { - assertResult( + assertJSXResult( ` Rock & Roll `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('span', {${devProps(2)}}, "Rock & Roll" ) `, + }, ); }); it("handles non-breaking spaces in JSX text", () => { /* eslint-disable no-irregular-whitespace */ - assertResult( + assertJSXResult( ` `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('span', {${devProps(2)}}, "a " ) `, + }, ); /* eslint-enable no-irregular-whitespace */ }); it("handles comment-only JSX interpolations", () => { - assertResult( + assertJSXResult( `
@@ -325,69 +416,77 @@ describe("transform JSX", () => {
; `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('div', {${devProps(2)}} , React.createElement('span', {${devProps(3)}} ) /* foo */ , React.createElement('span', {${devProps(5)}} ) ); `, + }, ); }); it("handles parsing object rest/spread", () => { - assertResult( + assertJSXResult( ` const foo = { ...bar, baz: , }; `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const foo = { ...bar, baz: React.createElement(Baz, {${devProps(4)}} ), }; `, + }, ); }); it("handles non-identifier prop names", () => { - assertResult( + assertJSXResult( `
`, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('div', { a: 1, 'data-id': 2, ${devProps(2)}} ) `, + }, ); }); it("handles multi-line prop strings", () => { - assertResult( + assertJSXResult( `
`, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('div', { value: "This is a multi-line string." , ${devProps(2)}} ) `, + }, ); }); it("handles leading and trailing spaces in multi-line prop strings", () => { - assertResult( + assertJSXResult( `
`, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('div', { value: " This is a longer multi-line string. " @@ -404,50 +504,60 @@ describe("transform JSX", () => { , ${devProps(2)}} ) `, + }, ); }); it("handles prop string values with entities", () => { - assertResult( + assertJSXResult( `
`, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('div', { value: "a>b", ${devProps(2)}} ) `, + }, ); }); it("handles handles boolean prop values", () => { - assertResult( + assertJSXResult( ` const e =
; `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const e = React.createElement('div', { a: true, ${devProps(2)}} ); `, + expectedClassicProdESMResult: ` + const e = React.createElement('div', { a: true,} ); + `, + }, ); }); it("preserves/allows windows newlines in string literals but not tag bodies", () => { - assertResult( + assertJSXResult( ` const e =
a\r\nb
; `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const e = React.createElement('div', { a: "foo\\r\\nbar" , __self: this, __source: {fileName: _jsxFileName, lineNumber: 2}}, "a b" ); `, + }, ); }); it("allows JSX fragment syntax", () => { - assertResult( + assertJSXResult( ` const f = ( <> @@ -456,7 +566,8 @@ describe("transform JSX", () => { ); `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const f = ( React.createElement(React.Fragment, null , React.createElement('div', {${devProps(4)}} ) @@ -464,11 +575,20 @@ describe("transform JSX", () => { ) ); `, + expectedClassicProdESMResult: ` + const f = ( + React.createElement(React.Fragment, null + , React.createElement('div', null ) + , React.createElement('span', null ) + ) + ); + `, + }, ); }); it("handles transformed react name in createElement and Fragment", () => { - assertResult( + assertJSXResult( ` import React from 'react'; const f = ( @@ -478,7 +598,8 @@ describe("transform JSX", () => { ); `, - `"use strict";${JSX_PREFIX}${IMPORT_DEFAULT_PREFIX} + { + expectedClassicDevCJSResult: `"use strict";${JSX_PREFIX}${IMPORT_DEFAULT_PREFIX} var _react = require('react'); var _react2 = _interopRequireDefault(_react); const f = ( _react2.default.createElement(_react2.default.Fragment, null @@ -487,12 +608,12 @@ describe("transform JSX", () => { ) ); `, - {extraTransforms: ["imports"]}, + }, ); }); - it("allows custom JSX pragmas", () => { - assertResult( + it("allows custom JSX pragmas for classic transform", () => { + assertJSXResult( ` const f = ( <> @@ -501,7 +622,8 @@ describe("transform JSX", () => { ); `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const f = ( h(Fragment, null , h('div', {__self: this, __source: {fileName: _jsxFileName, lineNumber: 4}} ) @@ -509,12 +631,13 @@ describe("transform JSX", () => { ) ); `, + }, {jsxPragma: "h", jsxFragmentPragma: "Fragment"}, ); }); - it("properly transforms imports for JSX pragmas", () => { - assertResult( + it("properly transforms imports for JSX pragmas in the classic transform", () => { + assertJSXResult( ` import {h, Fragment} from 'preact'; const f = ( @@ -524,7 +647,8 @@ describe("transform JSX", () => { ); `, - `"use strict";${JSX_PREFIX} + { + expectedClassicDevCJSResult: `"use strict";${JSX_PREFIX} var _preact = require('preact'); const f = ( _preact.h(_preact.Fragment, null @@ -533,31 +657,36 @@ describe("transform JSX", () => { ) ); `, - {extraTransforms: ["imports"], jsxPragma: "h", jsxFragmentPragma: "Fragment"}, + }, + {jsxPragma: "h", jsxFragmentPragma: "Fragment"}, ); }); it("allows empty fragments", () => { - assertResult( + assertJSXResult( ` const c = <>; `, - ` + { + expectedClassicDevESMResult: ` const c = React.createElement(React.Fragment, null); `, + }, ); }); it("allows fragments without whitespace", () => { - assertResult( + assertJSXResult( ` const c = <>
; `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const c = React.createElement(React.Fragment, null, React.createElement('a', {${devProps( 2, )}})); `, + }, ); }); @@ -566,98 +695,42 @@ describe("transform JSX", () => { }); it("handles spread children", () => { - assertResult( + assertJSXResult( ` const e = {...b}; `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} const e = React.createElement(A, {${devProps(2)}}, ...b); `, + }, ); }); - describe("with production true", () => { - it("handles no props", () => { - assertResult( - ` - - `, - ` - React.createElement(A, null ) - `, - {production: true}, - ); - }); - - it("handles props", () => { - assertResult( - ` - - `, - ` - React.createElement(A, { a: "b",} ) - `, - {production: true}, - ); - }); - - it("handles bool props", () => { - assertResult( - ` - - `, - ` - React.createElement(A, { a: true,} ) - `, - {production: true}, - ); - }); - - it("handles spread props", () => { - assertResult( - ` - - `, - ` - React.createElement(A, { ...obj,} ) - `, - {production: true}, - ); - }); - - it("handles fragment", () => { - assertResult( - ` - <>Hi - `, - ` - React.createElement(React.Fragment, null, "Hi") - `, - {production: true}, - ); - }); - }); - it("handles long HTML entities with many leading 0s", () => { // https://github.com/babel/babel/issues/14316 - assertResult( + assertJSXResult( `

`, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('div', {${devProps(2)}}, " \\u0014 " ) `, + }, ); }); it("does not allow prototype access in JSX entity handling", () => { - assertResult( + assertJSXResult( `
&valueOf; `, - `${JSX_PREFIX} + { + expectedClassicDevESMResult: `${JSX_PREFIX} React.createElement('a', {${devProps(2)}}, "&valueOf;") `, + }, ); }); }); diff --git a/test/util.ts b/test/util.ts index 7d855565..9596c178 100644 --- a/test/util.ts +++ b/test/util.ts @@ -12,10 +12,11 @@ export function assertExpectations( code: string, expectations: Expectations, options: Options, + message?: string, ): void { const resultCode = transform(code, options).code; if ("expectedResult" in expectations) { - assert.strictEqual(resultCode, expectations.expectedResult); + assert.strictEqual(resultCode, expectations.expectedResult, message); } if ("expectedOutput" in expectations) { const outputs: Array = []; @@ -25,8 +26,7 @@ export function assertExpectations( setOutput: (value: unknown) => outputs.push(JSON.parse(JSON.stringify(value))), }); assert.strictEqual(outputs.length, 1, "setOutput should be called exactly once"); - assert.deepStrictEqual([1, 2], [1, 2]); - assert.deepStrictEqual(outputs[0], expectations.expectedOutput); + assert.deepStrictEqual(outputs[0], expectations.expectedOutput, message); } } @@ -34,8 +34,9 @@ export function assertResult( code: string, expectedResult: string, options: Options = {transforms: ["jsx", "imports"]}, + message: string | undefined = undefined, ): void { - assertExpectations(code, {expectedResult}, options); + assertExpectations(code, {expectedResult}, options, message); } export function assertOutput(