From 7a2b6414f6fcf4c73d53420251d594fb7e82dc1b Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Tue, 16 Jul 2019 14:06:39 -0400 Subject: [PATCH 1/5] Allow starting dev server without opening browser --- DEVELOPERS.md | 9 +++++++++ scripts/start.js | 8 +++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 5955e386cb..0b51caa87e 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -58,6 +58,15 @@ to accomplish this. To start the server: When asked, provide a valid user name (in the form of `user@domain`) and password so the application can start in the context of a logged in user. +When the dev server is started, it will attempt to open a new browser window/tab on +your system default browser to the app's running URL. This behavior can be modified +by specifying the `BROWSER` environment variable. Possible values are: + BROWSER=none # disable the feature + BROWSER=google-chrome # open a new tab in chrome on Linux + BROWSER='google chrome' # open a new tab in chrome on MacOS + BROWSER=chrome # open a new tab in chrome on Windows + BROWSER=firefox # open a new tab in firefox on Linux + ### Build You can build the static assets from source by: diff --git a/scripts/start.js b/scripts/start.js index 0ee0419297..a116d9ac87 100644 --- a/scripts/start.js +++ b/scripts/start.js @@ -134,9 +134,11 @@ function openBrowser(port, protocol) { if (process.env.BROWSER) { options.app = process.env.BROWSER } - opn(protocol + '://localhost:' + port + '/', options).catch(err => { - // ignore errors - can happen when starting the server in docker container - }) + if (options.app !== 'none') { + opn(protocol + '://localhost:' + port + '/', options).catch(err => { + // ignore errors - can happen when starting the server in docker container + }) + } } // We need to provide a custom onError function for httpProxyMiddleware. From 341ef3eb7ec5e63ddd97dd82cd9f938a43d695b4 Mon Sep 17 00:00:00 2001 From: Bohdan Iakymets Date: Thu, 18 Jul 2019 13:27:34 +0200 Subject: [PATCH 2/5] Removed payload from actions --- src/actions/clusters.js | 5 +--- src/actions/hosts.js | 5 +--- src/actions/index.js | 35 +++++-------------------- src/actions/operatingSystems.js | 5 +--- src/actions/pendingTasks.js | 10 ++----- src/actions/pool.js | 6 +---- src/actions/storageDomains.js | 8 ++---- src/actions/templates.js | 5 +--- src/actions/userMessages.js | 5 +--- src/actions/vnicProfiles.js | 5 +--- src/components/OptionsDialog/actions.js | 5 +--- src/sagas/storageDomains.js | 2 +- src/sagas/utils.js | 4 +-- 13 files changed, 22 insertions(+), 78 deletions(-) diff --git a/src/actions/clusters.js b/src/actions/clusters.js index 817765b459..40a198325b 100644 --- a/src/actions/clusters.js +++ b/src/actions/clusters.js @@ -11,8 +11,5 @@ export function setClusters (clusters) { } export function getAllClusters () { - return { - type: GET_ALL_CLUSTERS, - payload: {}, - } + return { type: GET_ALL_CLUSTERS } } diff --git a/src/actions/hosts.js b/src/actions/hosts.js index 356ec09fa7..314235c5df 100644 --- a/src/actions/hosts.js +++ b/src/actions/hosts.js @@ -11,8 +11,5 @@ export function setHosts (hosts) { } export function getAllHosts () { - return { - type: GET_ALL_HOSTS, - payload: {}, - } + return { type: GET_ALL_HOSTS } } diff --git a/src/actions/index.js b/src/actions/index.js index 5c2a364048..e4c98d9d55 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -41,10 +41,7 @@ export * from './userMessages' export * from './disks' export function appConfigured () { - return { - type: APP_CONFIGURED, - payload: {}, - } + return { type: APP_CONFIGURED } } export function startSchedulerFixedDelay (delayInSeconds = AppConfiguration.schedulerFixedDelayInSeconds) { @@ -55,17 +52,11 @@ export function startSchedulerFixedDelay (delayInSeconds = AppConfiguration.sche } export function stopSchedulerFixedDelay () { - return { - type: STOP_SCHEDULER_FIXED_DELAY, - payload: {}, - } + return { type: STOP_SCHEDULER_FIXED_DELAY } } export function persistState () { - return { - type: PERSIST_STATE, - payload: {}, - } + return { type: PERSIST_STATE } } /** @@ -138,17 +129,11 @@ export function changePage ({ type, id }) { } export function checkTokenExpired () { - return { - type: CHECK_TOKEN_EXPIRED, - payload: {}, - } + return { type: CHECK_TOKEN_EXPIRED } } export function showTokenExpiredMessage () { - return { - type: SHOW_TOKEN_EXPIRED_MSG, - payload: {}, - } + return { type: SHOW_TOKEN_EXPIRED_MSG } } export function getByPage ({ page, shallowFetch = true }) { @@ -171,10 +156,7 @@ export function setUSBFilter ({ usbFilter }) { } export function getUSBFilter () { - return { - type: GET_USB_FILTER, - payload: {}, - } + return { type: GET_USB_FILTER } } /** @@ -203,10 +185,7 @@ export function setUserGroups ({ groups }) { } export function getUserGroups () { - return { - type: GET_USER_GROUPS, - payload: {}, - } + return { type: GET_USER_GROUPS } } export function setCpuTopologyOptions ({ diff --git a/src/actions/operatingSystems.js b/src/actions/operatingSystems.js index 09e01a5d8c..855f4e6219 100644 --- a/src/actions/operatingSystems.js +++ b/src/actions/operatingSystems.js @@ -14,8 +14,5 @@ export function setOperatingSystems (operatingSystems) { } export function getAllOperatingSystems () { - return { - type: GET_ALL_OS, - payload: {}, - } + return { type: GET_ALL_OS } } diff --git a/src/actions/pendingTasks.js b/src/actions/pendingTasks.js index b27cdded6a..84e1996fb7 100644 --- a/src/actions/pendingTasks.js +++ b/src/actions/pendingTasks.js @@ -61,10 +61,7 @@ export function addSnapshotRestorePendingTask () { } export function removeSnapshotRestorePendingTask () { - return { - type: REMOVE_SNAPSHOT_RESTORE_PENDING_TASK, - payload: {}, - } + return { type: REMOVE_SNAPSHOT_RESTORE_PENDING_TASK } } export function addSnapshotAddPendingTask () { @@ -78,8 +75,5 @@ export function addSnapshotAddPendingTask () { } export function removeSnapshotAddPendingTask () { - return { - type: REMOVE_SNAPSHOT_ADD_PENDING_TASK, - payload: {}, - } + return { type: REMOVE_SNAPSHOT_ADD_PENDING_TASK } } diff --git a/src/actions/pool.js b/src/actions/pool.js index 7cbcd7f28f..d2167c22b8 100644 --- a/src/actions/pool.js +++ b/src/actions/pool.js @@ -90,11 +90,7 @@ export function getSinglePool ({ poolId }) { } export function updateVmsPoolsCount () { - return { - type: UPDATE_VMPOOLS_COUNT, - payload: { - }, - } + return { type: UPDATE_VMPOOLS_COUNT } } export function poolActionInProgress ({ poolId, name, started }) { diff --git a/src/actions/storageDomains.js b/src/actions/storageDomains.js index 893202cd00..8adfe36fad 100644 --- a/src/actions/storageDomains.js +++ b/src/actions/storageDomains.js @@ -8,15 +8,11 @@ import { } from '_/constants' export function getAllStorageDomains (): Object { - return { - type: GET_ALL_STORAGE_DOMAINS, - } + return { type: GET_ALL_STORAGE_DOMAINS } } export function getIsoFiles (): Object { - return { - type: GET_ISO_FILES, - } + return { type: GET_ISO_FILES } } export function setStorageDomains (storageDomains: Array): Object { diff --git a/src/actions/templates.js b/src/actions/templates.js index 825e93b09a..85705e3bfd 100644 --- a/src/actions/templates.js +++ b/src/actions/templates.js @@ -14,8 +14,5 @@ export function setTemplates (templates) { } export function getAllTemplates () { - return { - type: GET_ALL_TEMPLATES, - payload: {}, - } + return { type: GET_ALL_TEMPLATES } } diff --git a/src/actions/userMessages.js b/src/actions/userMessages.js index 37055df484..0bfa4b8833 100644 --- a/src/actions/userMessages.js +++ b/src/actions/userMessages.js @@ -17,10 +17,7 @@ export function addUserMessage ({ message, shortMessage, type = '' }) { } export function clearUserMessages () { - return { - type: CLEAR_USER_MSGS, - payload: {}, - } + return { type: CLEAR_USER_MSGS } } export function setNotificationNotified ({ time }) { diff --git a/src/actions/vnicProfiles.js b/src/actions/vnicProfiles.js index b613cc4276..3a3778a0be 100644 --- a/src/actions/vnicProfiles.js +++ b/src/actions/vnicProfiles.js @@ -14,10 +14,7 @@ export function setVnicProfiles ({ vnicProfiles }) { } export function getAllVnicProfiles () { - return { - type: GET_ALL_VNIC_PROFILES, - payload: {}, - } + return { type: GET_ALL_VNIC_PROFILES } } export function addNetworksToVnicProfiles ({ networks }) { diff --git a/src/components/OptionsDialog/actions.js b/src/components/OptionsDialog/actions.js index 0e918c1d15..318852942e 100644 --- a/src/components/OptionsDialog/actions.js +++ b/src/components/OptionsDialog/actions.js @@ -31,8 +31,5 @@ export function getSSHKey ({ userId }) { } export function setUnloaded () { - return { - type: SET_UNLOADED, - payload: {}, - } + return { type: SET_UNLOADED } } diff --git a/src/sagas/storageDomains.js b/src/sagas/storageDomains.js index ce5932c103..7a9ad70fbd 100644 --- a/src/sagas/storageDomains.js +++ b/src/sagas/storageDomains.js @@ -88,7 +88,7 @@ function mergeStorageDomains (storageDomainsInternal) { */ export function* fetchIsoFiles (action) { // fetch ISO disk images and distribute them to their storage domains as files - const images = yield callExternalAction('getIsoImages', Api.getIsoImages, { payload: {} }) + const images = yield callExternalAction('getIsoImages', Api.getIsoImages) if (images && images.disk) { const storageDomainToDisks = images.disk.reduce( (acc, disk) => { diff --git a/src/sagas/utils.js b/src/sagas/utils.js index e252ced2ff..347ddd75bb 100644 --- a/src/sagas/utils.js +++ b/src/sagas/utils.js @@ -40,10 +40,10 @@ export function compareVersion (actual, required) { return false } -export function* callExternalAction (methodName, method, action, canBeMissing = false) { +export function* callExternalAction (methodName, method, action = {}, canBeMissing = false) { try { console.log(`External action: ${JSON.stringify(hidePassword({ action }))}, API method: ${methodName}()`) - const result = yield call(method, action.payload) + const result = yield call(method, action.payload || {}) return result } catch (e) { if (!canBeMissing) { From 7745188630d3f7a357fcf28576d8abc394942099 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Tue, 18 Jun 2019 13:26:20 -0400 Subject: [PATCH 3/5] Allow `FieldValue` component to have undefined children/tooltips When rendering a VM that is not running, the VM's __host__ is undefined. This causes the `FieldValue` proptype checker to throw an error on the browser console. The child value is in the JSX code, but the value is `undefined`. Updated the component's prop type such that `children` and `tooltips` must both be defined or must both be undefined. If they are not defined, don't bother rendering anything. --- .../VmDetails/cards/DetailsCard/FieldValue.js | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/components/VmDetails/cards/DetailsCard/FieldValue.js b/src/components/VmDetails/cards/DetailsCard/FieldValue.js index 8cf29e7cee..8e586f09e7 100644 --- a/src/components/VmDetails/cards/DetailsCard/FieldValue.js +++ b/src/components/VmDetails/cards/DetailsCard/FieldValue.js @@ -12,11 +12,25 @@ class FieldValue extends React.Component { this.updateOverflow = this.updateOverflow.bind(this) } + componentDidUpdate () { this.updateOverflow() } + componentDidMount () { + window.addEventListener('resize', this.updateOverflow) + this.updateOverflow() + } + + componentWillUnmount () { + window.removeEventListener('resize', this.updateOverflow) + } + updateOverflow () { + if (!this.props.children) { + return + } + const state = { isOverflow: false } if (this.ref.current.offsetWidth < this.ref.current.scrollWidth) { state.isOverflow = true @@ -26,25 +40,36 @@ class FieldValue extends React.Component { } } - componentDidMount () { - window.addEventListener('resize', this.updateOverflow) - this.updateOverflow() - } - - componentWillUnmount () { - window.removeEventListener('resize', this.updateOverflow) - } render () { const { children, tooltip } = this.props - return - {children} - + + if (children) { + return + {children} + + } else { + return null + } } } FieldValue.propTypes = { - children: PropTypes.oneOfType([ PropTypes.string, PropTypes.node ]).isRequired, - tooltip: PropTypes.string.isRequired, + children: function (props, propName, componentName, ...rest) { + if ((props.children && !props.tooltip) || (!props.children && props.tooltip)) { + return new Error(`Props 'children' and 'tooltip' are both required for component ${componentName}`) + } + return PropTypes.oneOfType([ PropTypes.string, PropTypes.node ])(props, propName, componentName, ...rest) + }, + tooltip: function (props, propName, componentName, ...rest) { + if ((props.children && !props.tooltip) || (!props.children && props.tooltip)) { + return new Error(`Props 'children' and 'tooltip' are both required for component ${componentName}`) + } + return PropTypes.string(props, propName, componentName, ...rest) + }, } export default FieldValue From e99addaf6d710532721afa5bf4ca6874be4f7458 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Tue, 2 Jul 2019 14:22:22 -0400 Subject: [PATCH 4/5] FieldValue, add props and move to EllipsisValue/index.js Moved `src/component/VmDetails/cards/DetailsCard/FieldValue.js` to `src/component/EllipsisValue.js`. Added props to `EllipsisValue` to make it easier to use in different components: - id - className --- package.json | 3 ++- .../FieldValue.js => EllipsisValue/index.js} | 8 ++++++-- src/components/EllipsisValue/style.css | 5 +++++ .../VmDetails/cards/DetailsCard/index.js | 15 +++++++-------- .../VmDetails/cards/DetailsCard/style.css | 6 ------ yarn.lock | 2 +- 6 files changed, 21 insertions(+), 18 deletions(-) rename src/components/{VmDetails/cards/DetailsCard/FieldValue.js => EllipsisValue/index.js} (89%) create mode 100644 src/components/EllipsisValue/style.css diff --git a/package.json b/package.json index e4d5bde787..67cd9e5fae 100644 --- a/package.json +++ b/package.json @@ -68,12 +68,13 @@ "whatwg-fetch": "1.0.0" }, "dependencies": { - "babel-polyfill": "6.26.0", "@patternfly/react-console": "1.10.22", + "babel-polyfill": "6.26.0", "blob-util": "1.2.1", "bootstrap": "3.4.1", "bootstrap-select": "1.13.1", "bootstrap-switch": "3.3.4", + "classnames": "2.2.6", "connected-react-router": "4.3.0", "history": "4.7.2", "immutable": "3.8.2", diff --git a/src/components/VmDetails/cards/DetailsCard/FieldValue.js b/src/components/EllipsisValue/index.js similarity index 89% rename from src/components/VmDetails/cards/DetailsCard/FieldValue.js rename to src/components/EllipsisValue/index.js index 8e586f09e7..1ed3fda4ba 100644 --- a/src/components/VmDetails/cards/DetailsCard/FieldValue.js +++ b/src/components/EllipsisValue/index.js @@ -1,6 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import style from './style.css' +import classnames from 'classnames' class FieldValue extends React.Component { constructor (props) { @@ -41,11 +42,12 @@ class FieldValue extends React.Component { } render () { - const { children, tooltip } = this.props + const { className, id, children, tooltip } = this.props if (children) { return @@ -58,6 +60,8 @@ class FieldValue extends React.Component { } FieldValue.propTypes = { + className: PropTypes.string, + id: PropTypes.string, children: function (props, propName, componentName, ...rest) { if ((props.children && !props.tooltip) || (!props.children && props.tooltip)) { return new Error(`Props 'children' and 'tooltip' are both required for component ${componentName}`) diff --git a/src/components/EllipsisValue/style.css b/src/components/EllipsisValue/style.css new file mode 100644 index 0000000000..28a6dca894 --- /dev/null +++ b/src/components/EllipsisValue/style.css @@ -0,0 +1,5 @@ +.field-value { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} diff --git a/src/components/VmDetails/cards/DetailsCard/index.js b/src/components/VmDetails/cards/DetailsCard/index.js index b4655e207d..a5e519d036 100644 --- a/src/components/VmDetails/cards/DetailsCard/index.js +++ b/src/components/VmDetails/cards/DetailsCard/index.js @@ -31,15 +31,14 @@ import SelectBox from '../../../SelectBox' import BaseCard from '../../BaseCard' import { Grid, Row, Col } from '_/components/Grid' -import CloudInit from './CloudInit' +import EllipsisValue from '_/components/EllipsisValue' +import ExpandCollapseSection from '_/components/ExpandCollapseSection' import style from './style.css' +import CloudInit from './CloudInit' import HotPlugChangeConfirmationModal from './HotPlugConfirmationModal' import NextRunChangeConfirmationModal from './NextRunChangeConfirmationModal' - -import ExpandCollapseSection from '../../../ExpandCollapseSection' -import FieldValue from './FieldValue' import FieldRow from './FieldRow' /* @@ -723,7 +722,7 @@ class DetailsCard extends React.Component { - { {hostName} || } + { {hostName} || } @@ -739,7 +738,7 @@ class DetailsCard extends React.Component { - { {fqdn} || } + { {fqdn} || } { !isFullEdit && clusterName } @@ -775,10 +774,10 @@ class DetailsCard extends React.Component { { templateName } - { !isEditing && {cdImageName} } + { !isEditing && {cdImageName} } { isEditing && !canChangeCd &&
- {cdImageName} + {cdImageName}
} diff --git a/src/components/VmDetails/cards/DetailsCard/style.css b/src/components/VmDetails/cards/DetailsCard/style.css index e8ce1d57e9..c2b44c8093 100644 --- a/src/components/VmDetails/cards/DetailsCard/style.css +++ b/src/components/VmDetails/cards/DetailsCard/style.css @@ -53,12 +53,6 @@ display: inline-block; } -.field-value { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} - button.field-level-help:global(.popover-pf-info) { flex-grow: 0; } diff --git a/yarn.lock b/yarn.lock index 0e5471392e..7007830ce0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1879,7 +1879,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.0, classnames@^2.2.5: +classnames@2.2.6, classnames@^2.2.0, classnames@^2.2.5: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" From c7d1b3cb77648545bd94ebb803d60f3d94673cc3 Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Mon, 8 Jul 2019 12:56:35 -0400 Subject: [PATCH 5/5] Fix component name, add 'propTypeExtras.js' - `propTypeExtras.js` can now be the common area for any custom prop-type validation needs. - `propTypeExtras.test.js` makes sure they work as expected and serves as a demo of how the extras will work --- src/components/EllipsisValue/index.js | 24 +++++----------- src/propTypeExtras.js | 23 +++++++++++++++ src/propTypeExtras.test.js | 41 +++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 src/propTypeExtras.js create mode 100644 src/propTypeExtras.test.js diff --git a/src/components/EllipsisValue/index.js b/src/components/EllipsisValue/index.js index 1ed3fda4ba..0c4be9ecb5 100644 --- a/src/components/EllipsisValue/index.js +++ b/src/components/EllipsisValue/index.js @@ -1,9 +1,10 @@ import React from 'react' import PropTypes from 'prop-types' +import { xor } from '_/propTypeExtras' import style from './style.css' import classnames from 'classnames' -class FieldValue extends React.Component { +class EllipsisValue extends React.Component { constructor (props) { super(props) this.ref = React.createRef() @@ -53,27 +54,16 @@ class FieldValue extends React.Component { > {children}
- } else { - return null } + return null } } -FieldValue.propTypes = { +EllipsisValue.propTypes = { className: PropTypes.string, id: PropTypes.string, - children: function (props, propName, componentName, ...rest) { - if ((props.children && !props.tooltip) || (!props.children && props.tooltip)) { - return new Error(`Props 'children' and 'tooltip' are both required for component ${componentName}`) - } - return PropTypes.oneOfType([ PropTypes.string, PropTypes.node ])(props, propName, componentName, ...rest) - }, - tooltip: function (props, propName, componentName, ...rest) { - if ((props.children && !props.tooltip) || (!props.children && props.tooltip)) { - return new Error(`Props 'children' and 'tooltip' are both required for component ${componentName}`) - } - return PropTypes.string(props, propName, componentName, ...rest) - }, + children: xor(PropTypes.oneOfType([ PropTypes.string, PropTypes.node ]), 'tooltip'), + tooltip: xor(PropTypes.string, 'children'), } -export default FieldValue +export default EllipsisValue diff --git a/src/propTypeExtras.js b/src/propTypeExtras.js new file mode 100644 index 0000000000..31456795de --- /dev/null +++ b/src/propTypeExtras.js @@ -0,0 +1,23 @@ +// @flow + +import PropTypes from 'prop-types' + +/** + * Validate the property by verifying the _xorPropName_ existence matches this prop's + * existence and then that the prop meets the _baseValidator_'s tests. + * + * Use this to ensure that both properties must be present or missing. + */ +const xor = + (baseValidator: PropTypes.Validator, xorPropName: string) => + (props: Object, propName: string, componentName: string, ...rest: Array): Error | null => { + if ((props[propName] && !props[xorPropName]) || (!props[propName] && props[xorPropName])) { + return new Error(`Props '${propName}' and '${xorPropName}' are both required for component '${componentName}'`) + } + + return baseValidator(props, propName, componentName, ...rest) + } + +export { + xor, +} diff --git a/src/propTypeExtras.test.js b/src/propTypeExtras.test.js new file mode 100644 index 0000000000..5c81be00f5 --- /dev/null +++ b/src/propTypeExtras.test.js @@ -0,0 +1,41 @@ +/* eslint-env jest */ + +import PropTypes from 'prop-types' +import { xor } from '_/propTypeExtras' + +describe('xor PropType cross property validation', () => { + const propTypesRest = [ 'prop', null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED" ] + + it('both exist, and self-verify', () => { + const p1 = xor(PropTypes.string, 'p2') + const p2 = xor(PropTypes.number, 'p1') + + const r1 = p1({ p1: 'foo', p2: 42 }, 'p1', 'Test', ...propTypesRest) + const r2 = p2({ p1: 'foo', p2: 42 }, 'p2', 'Test', ...propTypesRest) + + expect(r1).toBeNull() + expect(r2).toBeNull() + }) + + it('both exist, but one or the other does not self verify', () => { + const p1 = xor(PropTypes.string, 'p2') + const p2 = xor(PropTypes.number, 'p1') + + const r1 = p1({ p1: 42, p2: 'foo' }, 'p1', 'Test', ...propTypesRest) + const r2 = p2({ p1: 42, p2: 'foo' }, 'p2', 'Test', ...propTypesRest) + + expect(r1).toEqual(new Error('Invalid prop `p1` of type `number` supplied to `Test`, expected `string`.')) + expect(r2).toEqual(new Error('Invalid prop `p2` of type `string` supplied to `Test`, expected `number`.')) + }) + + it('one or the other exits, throw error', () => { + const p1 = xor(PropTypes.string, 'p2') + const p2 = xor(PropTypes.number, 'p1') + + const r1 = p1({ p1: 'foo' }, 'p1', 'Test', ...propTypesRest) + const r2 = p2({ p2: 42 }, 'p2', 'Test', ...propTypesRest) + + expect(r1).toEqual(new Error(`Props 'p1' and 'p2' are both required for component 'Test'`)) + expect(r2).toEqual(new Error(`Props 'p2' and 'p1' are both required for component 'Test'`)) + }) +})