= {
+ root: ({ props: p }): ICSSInJSStyle => ({
+ display: 'inline-flex',
+ position: 'relative',
+ alignItems: 'center',
+ outline: 0,
+ ...(p.fluid && { width: '100%' }),
+ }),
- return {
- display: 'inline-flex',
- position: 'relative',
- alignItems: 'center',
- outline: 0,
- ...(fluid && { width: '100%' }),
- }
- },
+ input: ({ props: p, variables: v }): ICSSInJSStyle => ({
+ outline: 0,
+ border: 0,
+ borderRadius: v.borderRadius,
+ borderBottom: v.borderBottom,
+ color: v.fontColor,
+ backgroundColor: v.backgroundColor,
+ padding: v.inputPadding,
+ ...(p.fluid && { width: '100%' }),
+ ...(p.inline && { float: 'left' }),
+ ':focus': {
+ borderColor: v.inputFocusBorderColor,
+ borderRadius: v.inputFocusBorderRadius,
+ },
+ }),
- input: ({ props, variables }): ICSSInJSStyle => {
- const { fluid, inline } = props
-
- return {
- outline: 0,
- border: 0,
- borderRadius: variables.borderRadius,
- borderBottom: variables.borderBottom,
- color: variables.fontColor,
- backgroundColor: variables.backgroundColor,
- padding: variables.inputPadding,
- ...(fluid && { width: '100%' }),
- ...(inline && { float: 'left' }),
- ':focus': {
- borderColor: variables.inputFocusBorderColor,
- borderRadius: variables.inputFocusBorderRadius,
- },
- }
- },
-
- icon: ({ props, variables }): ICSSInJSStyle => {
- return {
- position: variables.iconPosition,
- right: variables.iconRight,
- outline: 0,
- }
- },
+ icon: ({ variables: v }): ICSSInJSStyle => ({
+ position: v.iconPosition as PositionProperty,
+ right: v.iconRight,
+ outline: 0,
+ }),
}
export default inputStyles
diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts
index 060bd6b50d..5f65f7188d 100644
--- a/src/themes/teams/components/Input/inputVariables.ts
+++ b/src/themes/teams/components/Input/inputVariables.ts
@@ -1,21 +1,33 @@
import { pxToRem } from '../../../../lib'
+export interface IInputVariables {
+ borderRadius: string
+ borderBottom: string
+ backgroundColor: string
+ fontColor: string
+ fontSize: string
+ iconPosition: string
+ iconRight: string
+ inputPadding: string
+ inputFocusBorderColor: string
+ inputFocusBorderRadius: string
+}
-export default (siteVars: any) => {
- const vars: any = {}
-
- vars.borderRadius = `${pxToRem(3)}`
- vars.borderBottom = `${pxToRem(2)} solid transparent`
- vars.backgroundColor = siteVars.gray10
+const [_2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem, _24px_asRem] = [2, 3, 6, 12, 24].map(v =>
+ pxToRem(v),
+)
- vars.fontColor = siteVars.bodyColor
- vars.fontSize = siteVars.fontSizes.medium
+export default (siteVars): IInputVariables => ({
+ borderRadius: _3px_asRem,
+ borderBottom: `${_2px_asRem} solid transparent`,
+ backgroundColor: siteVars.gray10,
- vars.inputPadding = `${pxToRem(6)} ${pxToRem(24)} ${pxToRem(6)} ${pxToRem(12)}`
- vars.inputFocusBorderColor = siteVars.brand
- vars.inputFocusBorderRadius = `${pxToRem(3)} ${pxToRem(3)} ${pxToRem(2)} ${pxToRem(2)}`
+ fontColor: siteVars.bodyColor,
+ fontSize: siteVars.fontSizes.medium,
- vars.iconPosition = 'absolute'
- vars.iconRight = `${pxToRem(2)}`
+ iconPosition: 'absolute',
+ iconRight: _2px_asRem,
- return vars
-}
+ inputPadding: `${_6px_asRem} ${_24px_asRem} ${_6px_asRem} ${_12px_asRem}`,
+ inputFocusBorderColor: siteVars.brand,
+ inputFocusBorderRadius: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`,
+})
diff --git a/test/specs/commonTests/implementsShorthandProp.tsx b/test/specs/commonTests/implementsShorthandProp.tsx
index 5873f2ca62..5a8fea42c4 100644
--- a/test/specs/commonTests/implementsShorthandProp.tsx
+++ b/test/specs/commonTests/implementsShorthandProp.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
-import { mount } from './isConformant'
+import { mount, ReactWrapper } from 'enzyme'
+import { IProps } from '../../../types/utils'
export type ShorthandTestOptions = {
mapsValueToProp?: string
@@ -18,34 +19,35 @@ export default Component => {
const { mapsValueToProp } = options
const { displayName } = ShorthandComponent
+ const checkPropsMatch = (props: IProps, matchedProps: IProps) =>
+ Object.keys(matchedProps).every(propName => matchedProps[propName] === props[propName])
+
+ const expectContainsSingleShorthandElement = (wrapper: ReactWrapper, withProps: IProps) =>
+ expect(
+ wrapper.findWhere(
+ node => node.type() === ShorthandComponent && checkPropsMatch(node.props(), withProps),
+ ).length,
+ ).toEqual(1)
+
+ const expectShorthandPropsAreHandled = (withProps: IProps | string) => {
+ const props = { [shorthandProp]: withProps }
+ const matchedProps =
+ typeof withProps === 'string' ? { [mapsValueToProp]: withProps } : withProps
+
+ expectContainsSingleShorthandElement(mount(), matchedProps)
+ }
+
describe(`shorthand property '${shorthandProp}' with default value of '${displayName}' component`, () => {
test(`is defined`, () => {
expect(Component.propTypes[shorthandProp]).toBeTruthy()
})
test(`string value is handled as ${displayName}'s ${mapsValueToProp}`, () => {
- const props = { [shorthandProp]: 'some value' }
- const wrapper = mount()
-
- const shorthandComponentProps = wrapper.find(displayName).props()
- expect(shorthandComponentProps[mapsValueToProp]).toEqual('some value')
+ expectShorthandPropsAreHandled('shorthand prop value')
})
test(`object value is spread as ${displayName}'s props`, () => {
- const ShorthandValue = { foo: 'foo value', bar: 'bar value' }
-
- const props = { [shorthandProp]: ShorthandValue }
- const wrapper = mount()
-
- const shorthandComponentProps = wrapper.find(displayName).props()
-
- const allShorthandPropertiesArePassedToShorthandComponent = Object.keys(
- ShorthandValue,
- ).every(
- propertyName => ShorthandValue[propertyName] === shorthandComponentProps[propertyName],
- )
-
- expect(allShorthandPropertiesArePassedToShorthandComponent).toBe(true)
+ expectShorthandPropsAreHandled({ foo: 'foo value', bar: 'bar value' })
})
})
}
diff --git a/test/specs/commonTests/implementsWrapperProp.tsx b/test/specs/commonTests/implementsWrapperProp.tsx
new file mode 100644
index 0000000000..3efbccaa78
--- /dev/null
+++ b/test/specs/commonTests/implementsWrapperProp.tsx
@@ -0,0 +1,38 @@
+import * as React from 'react'
+import { mount, ReactWrapper } from 'enzyme'
+
+import Slot from 'src/components/Slot'
+import { ShorthandValue } from 'utils'
+
+export interface ImplementsWrapperPropOptions {
+ wrapppedComponentSelector: any
+ wrappperComponentSelector?: any
+}
+
+const implementsWrapperProp = (
+ Component: React.ReactType
,
+ options: ImplementsWrapperPropOptions,
+) => {
+ const { wrapppedComponentSelector, wrappperComponentSelector = Slot.defaultProps.as } = options
+
+ const wrapperTests = (wrapper: ReactWrapper) => {
+ expect(wrapper.length).toBeGreaterThan(0)
+ expect(wrapper.find(wrapppedComponentSelector).length).toBeGreaterThan(0)
+ }
+
+ describe('"wrapper" prop', () => {
+ it('wraps the component by default', () => {
+ wrapperTests(mount().find(wrappperComponentSelector))
+ })
+
+ it('wraps the component with a custom element', () => {
+ wrapperTests(mount(} />).find('span'))
+ })
+
+ it('wraps the component with a custom element using "as" prop', () => {
+ wrapperTests(mount().find('p'))
+ })
+ })
+}
+
+export default implementsWrapperProp
diff --git a/test/specs/commonTests/index.ts b/test/specs/commonTests/index.ts
index 9234d5fe96..cc420b2043 100644
--- a/test/specs/commonTests/index.ts
+++ b/test/specs/commonTests/index.ts
@@ -4,6 +4,7 @@ export { default as hasUIClassName } from './hasUIClassName'
export * from './implementsClassNameProps'
export { default as implementsCreateMethod } from './implementsCreateMethod'
export { default as implementsShorthandProp } from './implementsShorthandProp'
+export { default as implementsWrapperProp } from './implementsWrapperProp'
export { default as handlesAccessibility, getRenderedAttribute } from './handlesAccessibility'
diff --git a/test/specs/commonTests/isConformant.tsx b/test/specs/commonTests/isConformant.tsx
index cae25f0cc5..e6a0f4ad52 100644
--- a/test/specs/commonTests/isConformant.tsx
+++ b/test/specs/commonTests/isConformant.tsx
@@ -1,6 +1,6 @@
import * as _ from 'lodash'
import * as React from 'react'
-import { mount as enzymeMount } from 'enzyme'
+import { mount as enzymeMount, ReactWrapper } from 'enzyme'
import * as ReactDOMServer from 'react-dom/server'
import { ThemeProvider } from 'react-fela'
@@ -32,7 +32,7 @@ export const mount = (node, options?) => {
* @param {React.Component|Function} Component A component that should conform.
* @param {Object} [options={}]
* @param {Object} [options.eventTargets={}] Map of events and the child component to target.
- * @param {boolean} [options.exportedAtTopLevel=false] Is this component exported as top level API
+ * @param {boolean} [options.exportedAtTopLevel=false] Is this component exported as top level API?
* @param {boolean} [options.rendersPortal=false] Does this component render a Portal powered component?
* @param {Object} [options.requiredProps={}] Props required to render Component without errors or warnings.
*/
@@ -48,7 +48,7 @@ export default (Component, options: IConformant = {}) => {
const componentType = typeof Component
// This is added because the component is mounted
- const getComponent = wrapper => {
+ const getComponent = (wrapper: ReactWrapper) => {
// FelaTheme wrapper and the component itself:
let component = wrapper
.childAt(0)
diff --git a/test/specs/components/Input/Input-test.tsx b/test/specs/components/Input/Input-test.tsx
index 12eb1675ab..595d13e1d8 100644
--- a/test/specs/components/Input/Input-test.tsx
+++ b/test/specs/components/Input/Input-test.tsx
@@ -1,30 +1,101 @@
import * as React from 'react'
-import { isConformant, implementsShorthandProp } from 'test/specs/commonTests'
+import { mount, ReactWrapper } from 'enzyme'
+import {
+ isConformant,
+ implementsShorthandProp,
+ implementsWrapperProp,
+} from 'test/specs/commonTests'
import Input from 'src/components/Input/Input'
import Icon from 'src/components/Icon/Icon'
-import { mountWithProvider } from 'test/utils'
+import Slot from 'src/components/Slot'
+
+const testValue = 'test value'
+const htmlInputAttrs = ['id', 'name', 'pattern', 'placeholder', 'type', 'value']
+
+const getInputAttrsObject = (testValue: string) =>
+ htmlInputAttrs.reduce((acc, attr) => {
+ acc[attr] = testValue
+ return acc
+ }, {})
+
+const getInputDomNode = (inputComp: ReactWrapper): HTMLInputElement =>
+ inputComp.find('input').getDOMNode() as HTMLInputElement
+
+const setUserInputValue = (inputComp: ReactWrapper, value: string) => {
+ inputComp.find('input').simulate('change', { target: { value } })
+}
describe('Input', () => {
- isConformant(Input, {
- eventTargets: {
- onChange: 'input',
- },
+ describe('conformance', () => {
+ isConformant(Input, {
+ eventTargets: { onChange: 'input' },
+ })
})
+
+ implementsShorthandProp(Input)('input', Slot, { mapsValueToProp: 'type' })
implementsShorthandProp(Input)('icon', Icon, { mapsValueToProp: 'name' })
- describe('input', () => {
- it('renders a text by default', () => {
- const input = mountWithProvider().find('input[type="text"]')
- expect(input).not.toBe(undefined)
+ describe('wrapper', () => {
+ implementsShorthandProp(Input)('wrapper', Slot, { mapsValueToProp: 'content' })
+ implementsWrapperProp(Input, { wrapppedComponentSelector: 'input' })
+ })
+
+ it('renders a text by default', () => {
+ const inputComp = mount()
+ expect(inputComp.find('input[type="text"]').length).toBeGreaterThan(0)
+ })
+
+ describe('input related HTML attribute', () => {
+ const inputAttrsObject = getInputAttrsObject(testValue)
+ const domNode = getInputDomNode(mount())
+
+ htmlInputAttrs.forEach(attr => {
+ it(`'${attr}' is set correctly to '${testValue}'`, () => {
+ expect(domNode[attr]).toEqual(testValue)
+ })
+ })
+ })
+
+ describe('auto-controlled', () => {
+ it('sets input value from user when the value prop is not set (non-controlled mode)', () => {
+ const inputComp = mount()
+ const domNode = getInputDomNode(inputComp)
+ setUserInputValue(inputComp, testValue)
+
+ expect(domNode.value).toEqual(testValue)
+ })
+
+ it('cannot set input value from user when the value prop is already set (controlled mode)', () => {
+ const controlledInputValue = 'controlled input value'
+ const inputComp = mount()
+ const domNode = getInputDomNode(inputComp)
+ setUserInputValue(inputComp, testValue)
+
+ expect(domNode.value).toEqual(controlledInputValue)
})
})
describe('icon', () => {
it('creates the Icon component when the icon shorthand is provided', () => {
- const input = mountWithProvider().find('Icon[name="close"]')
- expect(input).not.toBe(undefined)
+ const inputComp = mount()
+ expect(inputComp.find('Icon[name="search"]').length).toBeGreaterThan(0)
+ })
+
+ it('creates the "close" Icon component when the clearable prop is provided and the input has content, removes the icon and value when the icon is clicked', () => {
+ const inputComp = mount()
+ const domNode = getInputDomNode(inputComp)
+ setUserInputValue(inputComp, testValue) // user types into the input
+ const iconComp = inputComp.find('Icon[name="close"]')
+
+ expect(domNode.value).toEqual(testValue) // input value is the one typed by the user
+ expect(iconComp.length).toBeGreaterThan(0) // the 'x' icon appears
+
+ iconComp.simulate('click') // user clicks on 'x' icon
+
+ expect(domNode.value).toEqual('') // input value gets cleared
+ expect(inputComp.find('Icon[name="close"]').length).toEqual(0) // the 'x' icon disappears
})
})
})
diff --git a/test/specs/components/Slot/Slot-test.ts b/test/specs/components/Slot/Slot-test.ts
index 6354dcac11..1241df7774 100644
--- a/test/specs/components/Slot/Slot-test.ts
+++ b/test/specs/components/Slot/Slot-test.ts
@@ -1,6 +1,31 @@
-import { isConformant } from 'test/specs/commonTests'
+import { mount } from 'enzyme'
+
import Slot from 'src/components/Slot'
+import { isConformant } from 'test/specs/commonTests'
describe('Slot', () => {
- isConformant(Slot, { exportedAtTopLevel: false })
+ const createSlot = (factoryFn: Function, val, options?) =>
+ mount(factoryFn(val, options)).find(Slot)
+
+ describe('is conformant', () => {
+ isConformant(Slot, { exportedAtTopLevel: false })
+ })
+
+ it(`create renders a ${Slot.defaultProps.as} element with content prop`, () => {
+ const testContent = 'test content'
+ const slot = createSlot(Slot.create, testContent)
+ const { as, content } = slot.props()
+
+ expect(as).toEqual(Slot.defaultProps.as)
+ expect(content).toEqual(testContent)
+ })
+
+ it(`createHTMLInput renders an input element with type prop`, () => {
+ const testType = 'test type'
+ const slot = createSlot(Slot.createHTMLInput, testType)
+ const { as, type } = slot.props()
+
+ expect(as).toEqual('input')
+ expect(type).toEqual(testType)
+ })
})
diff --git a/yarn.lock b/yarn.lock
index 61ca87d589..e61fac43ed 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -52,11 +52,22 @@
dependencies:
any-observable "^0.3.0"
+"@types/cheerio@*":
+ version "0.22.9"
+ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.9.tgz#b5990152604c2ada749b7f88cab3476f21f39d7b"
+
"@types/classnames@^2.2.4":
version "2.2.4"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.4.tgz#d3ee9ebf714aa34006707b8f4a58fd46b642305a"
integrity sha512-UWUmNYhaIGDx8Kv0NSqFRwP6HWnBMXam4nBacOrjIiPBKKCdWMCe77+Nbn6rI9+Us9c+BhE26u84xeYQv2bKeA==
+"@types/enzyme@^3.1.14":
+ version "3.1.14"
+ resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.14.tgz#379c26205f6e0e272f3a51d6bbdd50071a9d03a6"
+ dependencies:
+ "@types/cheerio" "*"
+ "@types/react" "*"
+
"@types/faker@^4.1.3":
version "4.1.3"
resolved "https://registry.yarnpkg.com/@types/faker/-/faker-4.1.3.tgz#544398268b37248300dc428316daa6a7521bbd19"