diff --git a/jest.config.js b/jest.config.js index cb4e7a99267..1d0d38b65d1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -156,7 +156,7 @@ module.exports = { // snapshotSerializers: [], // The test environment that will be used for testing - testEnvironment: 'jsdom' + testEnvironment: 'jsdom', // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, @@ -165,15 +165,15 @@ module.exports = { // testLocationInResults: false, // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], + testMatch: [ + "**/?(*.)+(test).[tj]s?(x)" + ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "/node_modules/" - // ], + testPathIgnorePatterns: [ + "/node_modules/", + "\\.ssr\\.test\\.[tj]sx?$" + ] // The regexp pattern or array of patterns that Jest uses to detect test files // testRegex: [], diff --git a/jest.ssr.config.js b/jest.ssr.config.js new file mode 100644 index 00000000000..5c061e90ed0 --- /dev/null +++ b/jest.ssr.config.js @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + displayName: 'SSR', + + // A map from regular expressions to module names that allow to stub out resources with a single module + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/__mocks__/fileMock.js', + '\\.(css|styl)$': 'identity-obj-proxy', + '\\.\./Icon/.*$': '/__mocks__/iconMock.js' + }, + + // Run tests from one or more projects + projects: [''], + + // A list of paths to directories that Jest should use to search for files in + roots: [ + 'packages/' + ], + + // The test environment that will be used for testing + testEnvironment: require.resolve('@react-spectrum/test-utils/src/SSREnvironment'), + + // The glob patterns Jest uses to detect test files + testMatch: [ + "**/?(*.)+(ssr.test).[tj]s?(x)" + ] +}; diff --git a/package.json b/package.json index 4db39354603..443865876d5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "start:docs": "PARCEL_WORKER_BACKEND=process DOCS_ENV=dev parcel 'packages/@react-{spectrum,aria,stately}/*/docs/*.mdx' 'packages/dev/docs/pages/**/*.mdx'", "build:docs": "PARCEL_WORKER_BACKEND=process DOCS_ENV=staging parcel build 'packages/@react-{spectrum,aria,stately}/*/docs/*.mdx' 'packages/dev/docs/pages/**/*.mdx' --no-scope-hoist", "test": "yarn jest", + "test:ssr": "yarn jest --config jest.ssr.config.js", "ci-test": "yarn jest --maxWorkers=2", "lint": "yarn check-types && eslint packages --ext .js,.ts,.tsx && node scripts/lint-packages.js", "jest": "node scripts/jest.js", diff --git a/packages/@react-spectrum/button/test/Button.ssr.test.js b/packages/@react-spectrum/button/test/Button.ssr.test.js new file mode 100644 index 00000000000..da4fd39cc21 --- /dev/null +++ b/packages/@react-spectrum/button/test/Button.ssr.test.js @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {testSSR} from '@react-spectrum/test-utils'; + +describe('Button SSR', function () { + it.each` + Name | props + ${'ActionButton'} | ${{}} + ${'Button'} | ${{}} + ${'FieldButton'} | ${{}} + ${'LogicButton'} | ${{}} + `('$Name should render without errors', async function ({Name, props}) { + await testSSR(__filename, ` + import {${Name}} from '../'; + <${Name}>Button + `); + }); +}); diff --git a/packages/@react-spectrum/checkbox/test/Checkbox.ssr.test.js b/packages/@react-spectrum/checkbox/test/Checkbox.ssr.test.js new file mode 100644 index 00000000000..41f1c628c34 --- /dev/null +++ b/packages/@react-spectrum/checkbox/test/Checkbox.ssr.test.js @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {testSSR} from '@react-spectrum/test-utils'; + +describe('Checkbox SSR', function () { + it('should render without errors', async function () { + await testSSR(__filename, ` + import {Checkbox} from '../'; + Test + `); + }); +}); diff --git a/packages/@react-spectrum/dialog/test/Dialog.ssr.test.js b/packages/@react-spectrum/dialog/test/Dialog.ssr.test.js new file mode 100644 index 00000000000..61f902ddecc --- /dev/null +++ b/packages/@react-spectrum/dialog/test/Dialog.ssr.test.js @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {testSSR} from '@react-spectrum/test-utils'; + +describe('Dialog SSR', function () { + it('Dialog should render without errors', async function () { + await testSSR(__filename, ` + import {Dialog} from '../'; + + + contents + + `); + }); + + // TODO: AlertDialog, DialogTrigger +}); diff --git a/packages/@react-spectrum/divider/test/Divider.ssr.test.js b/packages/@react-spectrum/divider/test/Divider.ssr.test.js new file mode 100644 index 00000000000..c69701d20ba --- /dev/null +++ b/packages/@react-spectrum/divider/test/Divider.ssr.test.js @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {testSSR} from '@react-spectrum/test-utils'; + +describe('Divider SSR', function () { + it('should render without errors', async function () { + await testSSR(__filename, ` + import {Divider} from '../'; + + `); + }); +}); diff --git a/packages/@react-spectrum/form/test/Form.ssr.test.js b/packages/@react-spectrum/form/test/Form.ssr.test.js new file mode 100644 index 00000000000..f387bd18b01 --- /dev/null +++ b/packages/@react-spectrum/form/test/Form.ssr.test.js @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {testSSR} from '@react-spectrum/test-utils'; + +describe('Form SSR', function () { + it('should render without errors', async function () { + await testSSR(__filename, ` + import {Provider} from '@react-spectrum/provider'; + import {theme} from '@react-spectrum/theme-default'; + import {Form} from '../'; + + +
+ + `); + }); +}); diff --git a/packages/@react-spectrum/test-utils/package.json b/packages/@react-spectrum/test-utils/package.json index 521b5ae850c..1138b958731 100644 --- a/packages/@react-spectrum/test-utils/package.json +++ b/packages/@react-spectrum/test-utils/package.json @@ -32,11 +32,13 @@ "url": "https://github.com/adobe/react-spectrum" }, "dependencies": { - "@babel/runtime": "^7.6.2" + "@babel/runtime": "^7.6.2", + "resolve": "^1.17.0" }, "peerDependencies": { "@testing-library/react": "^8.0.1", - "react": "^16.8.0" + "react": "^16.8.0", + "react-dom": "^16.8.0" }, "devDependencies": { "@adobe/spectrum-css-temp": "^3.0.0-alpha.1" diff --git a/packages/@react-spectrum/test-utils/src/SSREnvironment.js b/packages/@react-spectrum/test-utils/src/SSREnvironment.js new file mode 100644 index 00000000000..eb6dce9b822 --- /dev/null +++ b/packages/@react-spectrum/test-utils/src/SSREnvironment.js @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +const NodeEnvironment = require('jest-environment-node'); +const {Worker} = require('worker_threads'); + +// Setup a single worker instance that's shared between test environments via a __SSR_WORKER__ global. +let worker = new Worker(__dirname + '/hydrateWorker.js'); +worker.unref(); + +class SSREnvironment extends NodeEnvironment { + async setup() { + await super.setup(); + this.global.__SSR_WORKER__ = worker; + } + + async teardown() { + await super.teardown(); + } +} + +module.exports = SSREnvironment; diff --git a/packages/@react-spectrum/test-utils/src/hydrateWorker.js b/packages/@react-spectrum/test-utils/src/hydrateWorker.js new file mode 100644 index 00000000000..78a1293fd74 --- /dev/null +++ b/packages/@react-spectrum/test-utils/src/hydrateWorker.js @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +const {JSDOM} = require('jsdom'); +const {parentPort} = require('worker_threads'); +const util = require('util'); +require('ignore-styles'); +require('@babel/register')({ + extensions: ['.js', '.ts', '.tsx'] +}); + +let {evaluate} = require('./ssrUtils'); + +let globalNames = new Set(Object.getOwnPropertyNames(global)); + +parentPort.on('message', message => { + // Setup JSDOM environment for the rendered HTML + let dom = new JSDOM(`
${message.html}
`); + + // Copy JSDOM globals into the node global object so React and others can access them. + Object.getOwnPropertyNames(dom.window).forEach(key => { + if (!globalNames.has(key)) { + Object.defineProperty(global, key, Object.getOwnPropertyDescriptor(dom.window, key)); + } + }); + + // Capture React errors/warning and make them fail the tests. + let errors = []; + console.error = console.warn = (...messages) => { + errors.push(util.format(...messages)); + }; + + // Evaluate the code to get a React element, and hydrate into the JSDOM. + let ReactDOM = require('react-dom'); + let container = document.querySelector('#root'); + let element = evaluate(message.source, message.filename); + ReactDOM.hydrate(element, container); + + parentPort.postMessage({errors}); +}); diff --git a/packages/@react-spectrum/test-utils/src/index.ts b/packages/@react-spectrum/test-utils/src/index.ts index 08bf8ec93ce..ffe0ab8b97a 100644 --- a/packages/@react-spectrum/test-utils/src/index.ts +++ b/packages/@react-spectrum/test-utils/src/index.ts @@ -13,3 +13,4 @@ /// export * from './triggerPress'; +export * from './testSSR'; diff --git a/packages/@react-spectrum/test-utils/src/ssrUtils.js b/packages/@react-spectrum/test-utils/src/ssrUtils.js new file mode 100644 index 00000000000..6f2bb1a17f1 --- /dev/null +++ b/packages/@react-spectrum/test-utils/src/ssrUtils.js @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import path from 'path'; +import React from 'react'; +import resolve from 'resolve'; +import vm from 'vm'; + +export function evaluate(code, filename) { + // Setup a context to use as the global object when evaluating code. + // It will include React, along with a fake `require` function that resolves + // relative to the filename that's passed in (the test script.) + let ctx = { + React, + require(id) { + let resolved = resolve.sync(id, { + basedir: path.dirname(filename), + extensions: ['.js', '.json', '.ts', '.tsx'] + }); + + return require(resolved); + } + }; + + vm.createContext(ctx); + return vm.runInContext(code, ctx); +} diff --git a/packages/@react-spectrum/test-utils/src/testSSR.js b/packages/@react-spectrum/test-utils/src/testSSR.js new file mode 100644 index 00000000000..d44e3b2b50b --- /dev/null +++ b/packages/@react-spectrum/test-utils/src/testSSR.js @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Can't `import` babel, have to require? +const babel = require('@babel/core'); +import {evaluate} from './ssrUtils'; +import ReactDOMServer from 'react-dom/server'; + +export async function testSSR(filename, source) { + // Transform the code with babel so JSX becomes JS. + source = babel.transformSync(source, {filename}).code; + + // Evaluate the code, and render the resulting JSX element to HTML. + let element = evaluate(source, filename); + let html = ReactDOMServer.renderToString(element); + + // Send the HTML along with the source code to the worker to be hydrated in a DOM environment. + return new Promise((resolve, reject) => { + let worker = global.__SSR_WORKER__; + worker.once('error', reject); + worker.once('message', message => { + if (message.errors.length > 0) { + reject(new Error(message.errors[0])); + } else { + resolve(); + } + }); + + worker.postMessage({html, source, filename}); + }); +} diff --git a/patches/@jest+types++chalk+3.0.0.patch b/patches/@jest+types++chalk+3.0.0.patch new file mode 100644 index 00000000000..05963bb0e8b --- /dev/null +++ b/patches/@jest+types++chalk+3.0.0.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/@jest/types/node_modules/chalk/index.d.ts b/node_modules/@jest/types/node_modules/chalk/index.d.ts +index 7e22c45..21b7ec4 100644 +--- a/node_modules/@jest/types/node_modules/chalk/index.d.ts ++++ b/node_modules/@jest/types/node_modules/chalk/index.d.ts +@@ -400,6 +400,7 @@ This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`. + */ + declare const chalk: chalk.Chalk & chalk.ChalkFunction & { + supportsColor: chalk.ColorSupport | false; ++ // @ts-ignore + Level: typeof LevelEnum; + Color: Color; + ForegroundColor: ForegroundColor;