native tests passing

This commit is contained in:
Evan Jacobs
2021-04-14 01:09:19 -04:00
parent 69f17afca5
commit bca61fc7e6
8 changed files with 218 additions and 208 deletions

View File

@@ -3,6 +3,7 @@ import {
Attrs, Attrs,
Interpolation, Interpolation,
IStyledComponentFactory, IStyledComponentFactory,
ShouldForwardProp,
StyledObject, StyledObject,
StyleFunction, StyleFunction,
WebTarget, WebTarget,
@@ -15,6 +16,7 @@ type Options = {
attrs?: Attrs[]; attrs?: Attrs[];
componentId?: string; componentId?: string;
displayName?: string; displayName?: string;
shouldForwardProp?: ShouldForwardProp;
}; };
export default function constructWithOptions< export default function constructWithOptions<

View File

@@ -13,7 +13,6 @@ import type {
import determineTheme from '../utils/determineTheme'; import determineTheme from '../utils/determineTheme';
import { EMPTY_ARRAY, EMPTY_OBJECT } from '../utils/empties'; import { EMPTY_ARRAY, EMPTY_OBJECT } from '../utils/empties';
import generateDisplayName from '../utils/generateDisplayName'; import generateDisplayName from '../utils/generateDisplayName';
import getComponentName from '../utils/getComponentName';
import isStyledComponent from '../utils/isStyledComponent'; import isStyledComponent from '../utils/isStyledComponent';
import merge from '../utils/mixinDeep'; import merge from '../utils/mixinDeep';
import { Theme, ThemeContext } from './ThemeProvider'; import { Theme, ThemeContext } from './ThemeProvider';
@@ -124,8 +123,8 @@ export default (InlineStyle: IInlineStyleConstructor) => {
*/ */
let WrappedStyledComponent: IStyledNativeComponent; let WrappedStyledComponent: IStyledNativeComponent;
// eslint-disable-next-line react-hooks/rules-of-hooks
const forwardRef = (props: ExtensibleObject, ref: React.Ref<any>) => const forwardRef = (props: ExtensibleObject, ref: React.Ref<any>) =>
// eslint-disable-next-line react-hooks/rules-of-hooks
useStyledComponentImpl(WrappedStyledComponent, props, ref); useStyledComponentImpl(WrappedStyledComponent, props, ref);
forwardRef.displayName = displayName; forwardRef.displayName = displayName;
@@ -139,21 +138,18 @@ export default (InlineStyle: IInlineStyleConstructor) => {
WrappedStyledComponent.displayName = displayName; WrappedStyledComponent.displayName = displayName;
WrappedStyledComponent.shouldForwardProp = shouldForwardProp; WrappedStyledComponent.shouldForwardProp = shouldForwardProp;
// @ts-expect-error we don't actually need this for anything other than detection of a styled-component
WrappedStyledComponent.styledComponentId = true;
// fold the underlying StyledComponent target up since we folded the styles // fold the underlying StyledComponent target up since we folded the styles
WrappedStyledComponent.target = isTargetStyledComp ? styledComponentTarget.target : target; WrappedStyledComponent.target = isTargetStyledComp ? styledComponentTarget.target : target;
WrappedStyledComponent.withComponent = function withComponent( WrappedStyledComponent.withComponent = function withComponent(
tag: IStyledNativeComponent['target'] tag: IStyledNativeComponent['target']
) { ) {
const { componentId: previousComponentId, ...optionsToCopy } = options;
const newComponentId =
previousComponentId && `${previousComponentId}-${escape(getComponentName(tag))}`;
const newOptions = { const newOptions = {
...optionsToCopy, ...options,
attrs: finalAttrs, attrs: finalAttrs,
componentId: newComponentId,
}; };
return createStyledNativeComponent(tag, newOptions, rules); return createStyledNativeComponent(tag, newOptions, rules);

View File

@@ -8,6 +8,7 @@ import ThemeProvider, { ThemeConsumer, ThemeContext } from '../models/ThemeProvi
import { WebTarget } from '../types'; import { WebTarget } from '../types';
import isStyledComponent from '../utils/isStyledComponent'; import isStyledComponent from '../utils/isStyledComponent';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const reactNative = require('react-native'); const reactNative = require('react-native');
const InlineStyle = _InlineStyle(reactNative.StyleSheet); const InlineStyle = _InlineStyle(reactNative.StyleSheet);
@@ -16,17 +17,61 @@ const styled = (tag: WebTarget) => constructWithOptions(StyledNativeComponent, t
/* React native lazy-requires each of these modules for some reason, so let's /* React native lazy-requires each of these modules for some reason, so let's
* assume it's for a good reason and not eagerly load them all */ * assume it's for a good reason and not eagerly load them all */
const aliases = `ActivityIndicator ActivityIndicatorIOS ART Button DatePickerIOS DrawerLayoutAndroid const aliases = [
Image ImageBackground ImageEditor ImageStore KeyboardAvoidingView ListView MapView Modal NavigatorIOS 'ActivityIndicator',
Picker PickerIOS ProgressBarAndroid ProgressViewIOS ScrollView SegmentedControlIOS Slider 'ActivityIndicatorIOS',
SliderIOS SnapshotViewIOS Switch RecyclerViewBackedScrollView RefreshControl SafeAreaView StatusBar 'ART',
SwipeableListView SwitchAndroid SwitchIOS TabBarIOS Text TextInput ToastAndroid ToolbarAndroid 'Button',
Touchable TouchableHighlight TouchableNativeFeedback TouchableOpacity TouchableWithoutFeedback 'DatePickerIOS',
View ViewPagerAndroid WebView FlatList SectionList VirtualizedList Pressable`; 'DrawerLayoutAndroid',
'FlatList',
'Image',
'ImageBackground',
'ImageEditor',
'ImageStore',
'KeyboardAvoidingView',
'ListView',
'MapView',
'Modal',
'NavigatorIOS',
'Picker',
'PickerIOS',
'Pressable',
'ProgressBarAndroid',
'ProgressViewIOS',
'RecyclerViewBackedScrollView',
'RefreshControl',
'SafeAreaView',
'ScrollView',
'SectionList',
'SegmentedControlIOS',
'Slider',
'SliderIOS',
'SnapshotViewIOS',
'StatusBar',
'SwipeableListView',
'Switch',
'SwitchAndroid',
'SwitchIOS',
'TabBarIOS',
'Text',
'TextInput',
'ToastAndroid',
'ToolbarAndroid',
'Touchable',
'TouchableHighlight',
'TouchableNativeFeedback',
'TouchableOpacity',
'TouchableWithoutFeedback',
'View',
'ViewPagerAndroid',
'VirtualizedList',
'WebView',
] as const;
/* Define a getter for each alias which simply gets the reactNative component /* Define a getter for each alias which simply gets the reactNative component
* and passes it to styled */ * and passes it to styled */
aliases.split(/\s+/m).forEach(alias => aliases.forEach(alias =>
Object.defineProperty(styled, alias, { Object.defineProperty(styled, alias, {
enumerable: true, enumerable: true,
configurable: false, configurable: false,
@@ -37,4 +82,8 @@ aliases.split(/\s+/m).forEach(alias =>
); );
export { css, isStyledComponent, ThemeProvider, ThemeConsumer, ThemeContext, withTheme, useTheme }; export { css, isStyledComponent, ThemeProvider, ThemeConsumer, ThemeContext, withTheme, useTheme };
export default styled;
export default styled as typeof styled &
{
[key in typeof aliases[number]]: ReturnType<typeof constructWithOptions>;
};

View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`native expanded API should omit transient props 1`] = `
<Text
style={
Array [
Object {
"color": "red",
},
]
}
/>
`;
exports[`native expanded API withComponent should work 1`] = `
<Text
style={
Array [
Object {},
]
}
/>
`;
exports[`native expanded API withComponent should work 2`] = `
<View
style={
Array [
Object {},
]
}
/>
`;

View File

@@ -1,11 +1,8 @@
/* eslint-disable no-console, react/jsx-key, @typescript-eslint/no-empty-function */
import React, { PropsWithChildren } from 'react';
/* eslint-disable no-console */ import { Text, View } from 'react-native';
import { Text, View } from "react-native"; import TestRenderer from 'react-test-renderer';
import React from "react"; import styled, { ThemeProvider } from '../';
import TestRenderer from "react-test-renderer";
import styled, { ThemeProvider } from "../index";
// NOTE: These tests are like the ones for Web but a "light-version" of them // NOTE: These tests are like the ones for Web but a "light-version" of them
// This is mostly due to the similar logic // This is mostly due to the similar logic
@@ -16,7 +13,6 @@ describe('native', () => {
const FunctionalComponent = () => <View />; const FunctionalComponent = () => <View />;
class ClassComponent extends React.Component { class ClassComponent extends React.Component {
render() { render() {
return <View />; return <View />;
} }
@@ -33,18 +29,24 @@ describe('native', () => {
it('should throw a meaningful error when called with an invalid element', () => { it('should throw a meaningful error when called with an invalid element', () => {
const FunctionalComponent = () => <View />; const FunctionalComponent = () => <View />;
class ClassComponent extends React.Component { class ClassComponent extends React.Component {
render() { render() {
return <View />; return <View />;
} }
} }
const invalidComps = [undefined, null, 123, [], <View />, <FunctionalComponent />, <ClassComponent />]; const invalidComps = [
undefined,
null,
123,
[],
<View />,
<FunctionalComponent />,
<ClassComponent />,
];
invalidComps.forEach(comp => { invalidComps.forEach(comp => {
expect(() => { expect(() => {
// $FlowInvalidInputTest // @ts-expect-error invalid input test
const Comp = styled(comp)``; const Comp = styled(comp)``;
TestRenderer.create(<Comp />); TestRenderer.create(<Comp />);
// $FlowInvalidInputTest
}).toThrow(`Cannot create styled-component for component: ${comp}`); }).toThrow(`Cannot create styled-component for component: ${comp}`);
}); });
}); });
@@ -52,7 +54,7 @@ describe('native', () => {
it('should generate inline styles', () => { it('should generate inline styles', () => {
const Comp = styled.View``; const Comp = styled.View``;
const wrapper = TestRenderer.create(<Comp />); const wrapper = TestRenderer.create(<Comp />);
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType(View);
expect(view.props.style).toEqual([{}]); expect(view.props.style).toEqual([{}]);
}); });
@@ -67,7 +69,7 @@ describe('native', () => {
`; `;
const wrapper = TestRenderer.create(<Comp2 />); const wrapper = TestRenderer.create(<Comp2 />);
const view = wrapper.root.findByType('Text'); const view = wrapper.root.findByType(Text);
expect(view.props.style).toEqual([{ color: 'red', textAlign: 'left' }]); expect(view.props.style).toEqual([{ color: 'red', textAlign: 'left' }]);
}); });
@@ -77,36 +79,36 @@ describe('native', () => {
Inner.defaultProps = { Inner.defaultProps = {
theme: { theme: {
fontSize: 12 fontSize: 12,
}, },
style: { style: {
background: 'blue', background: 'blue',
textAlign: 'center' textAlign: 'center',
} },
}; };
const Outer = styled(Inner)``; const Outer = styled(Inner)``;
Outer.defaultProps = { Outer.defaultProps = {
theme: { theme: {
fontSize: 16 fontSize: 16,
}, },
style: { style: {
background: 'silver' background: 'silver',
} },
}; };
expect(Outer.defaultProps).toMatchInlineSnapshot(` expect(Outer.defaultProps).toMatchInlineSnapshot(`
Object { Object {
"style": Object { "style": Object {
"background": "silver", "background": "silver",
"textAlign": "center", "textAlign": "center",
}, },
"theme": Object { "theme": Object {
"fontSize": 16, "fontSize": 16,
}, },
} }
`); `);
}); });
it('should combine inline styles and the style prop', () => { it('should combine inline styles and the style prop', () => {
@@ -116,7 +118,7 @@ Object {
const style = { opacity: 0.9 }; const style = { opacity: 0.9 };
const wrapper = TestRenderer.create(<Comp style={style} />); const wrapper = TestRenderer.create(<Comp style={style} />);
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType(View);
expect(view.props.style).toEqual([{ paddingTop: 10 }, style]); expect(view.props.style).toEqual([{ paddingTop: 10 }, style]);
}); });
@@ -145,18 +147,15 @@ Object {
const wrapper = TestRenderer.create(<Comp opacity={0.5} />); const wrapper = TestRenderer.create(<Comp opacity={0.5} />);
expect(wrapper.root.findByType('View').props.style).toEqual([{ paddingTop: 5, opacity: 0.5 }]); expect(wrapper.root.findByType(View).props.style).toEqual([{ paddingTop: 5, opacity: 0.5 }]);
wrapper.update(<Comp opacity={0.9} />); wrapper.update(<Comp opacity={0.9} />);
expect(wrapper.root.findByType('View').props.style).toEqual([{ paddingTop: 5, opacity: 0.9 }]); expect(wrapper.root.findByType(View).props.style).toEqual([{ paddingTop: 5, opacity: 0.9 }]);
}); });
it('should forward the "as" prop if "forwardedAs" is used', () => { it('should forward the "as" prop if "forwardedAs" is used', () => {
const Comp = ({ const Comp = ({ as: Component = View, ...props }) => <Component {...props} />;
as: Component = View,
...props
}) => <Component {...props} />;
const Comp2 = styled(Comp)` const Comp2 = styled(Comp)`
background: red; background: red;
@@ -164,7 +163,7 @@ Object {
const wrapper = TestRenderer.create(<Comp2 forwardedAs={Text} />); const wrapper = TestRenderer.create(<Comp2 forwardedAs={Text} />);
expect(wrapper.root.findByType('Text')).not.toBeUndefined(); expect(wrapper.root.findByType(Text)).not.toBeUndefined();
}); });
describe('attrs', () => { describe('attrs', () => {
@@ -173,201 +172,199 @@ Object {
it('works fine with an empty object', () => { it('works fine with an empty object', () => {
const Comp = styled.View.attrs(() => ({}))``; const Comp = styled.View.attrs(() => ({}))``;
const wrapper = TestRenderer.create(<Comp />); const wrapper = TestRenderer.create(<Comp />);
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType(View);
expect(view.props).toEqual({ expect(view.props).toEqual({
style: [{}] style: [{}],
}); });
}); });
it('passes simple props on', () => { it('passes simple props on', () => {
const Comp = styled.View.attrs(() => ({ const Comp = styled.View.attrs(() => ({
test: true test: true,
}))``; }))``;
const wrapper = TestRenderer.create(<Comp />); const wrapper = TestRenderer.create(<Comp />);
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType(View);
expect(view.props).toEqual({ expect(view.props).toEqual({
style: [{}], style: [{}],
test: true test: true,
}); });
}); });
it('calls an attr-function with context', () => { it('calls an attr-function with context', () => {
const Comp = styled.View.attrs(p => ({ const Comp = styled.View.attrs(p => ({
copy: p.test copy: p.test,
}))``; }))``;
const test = 'Put that cookie down!'; const test = 'Put that cookie down!';
const wrapper = TestRenderer.create(<Comp test={test} />); const wrapper = TestRenderer.create(<Comp test={test} />);
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType(View);
expect(view.props).toEqual({ expect(view.props).toEqual({
style: [{}], style: [{}],
copy: test, copy: test,
test test,
}); });
}); });
it('merges multiple calls', () => { it('merges multiple calls', () => {
const Comp = styled.View.attrs(() => ({ const Comp = styled.View.attrs(() => ({
first: 'first', first: 'first',
test: '_' test: '_',
})).attrs(() => ({ })).attrs(() => ({
second: 'second', second: 'second',
test: 'test' test: 'test',
}))``; }))``;
const wrapper = TestRenderer.create(<Comp />); const wrapper = TestRenderer.create(<Comp />);
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType(View);
expect(view.props).toEqual({ expect(view.props).toEqual({
style: [{}], style: [{}],
first: 'first', first: 'first',
second: 'second', second: 'second',
test: 'test' test: 'test',
}); });
}); });
it('merges multiple fn calls', () => { it('merges multiple fn calls', () => {
const Comp = styled.View.attrs(() => ({ const Comp = styled.View.attrs(() => ({
first: 'first', first: 'first',
test: '_' test: '_',
})).attrs(() => ({ })).attrs(() => ({
second: 'second', second: 'second',
test: 'test' test: 'test',
}))``; }))``;
const wrapper = TestRenderer.create(<Comp />); const wrapper = TestRenderer.create(<Comp />);
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType(View);
expect(view.props).toEqual({ expect(view.props).toEqual({
style: [{}], style: [{}],
first: 'first', first: 'first',
second: 'second', second: 'second',
test: 'test' test: 'test',
}); });
}); });
it('merges attrs when inheriting SC', () => { it('merges attrs when inheriting SC', () => {
const Parent = styled.View.attrs(() => ({ const Parent = styled.View.attrs(() => ({
first: 'first' first: 'first',
}))``; }))``;
const Child = styled(Parent).attrs(() => ({ const Child = styled(Parent).attrs(() => ({
second: 'second' second: 'second',
}))``; }))``;
const wrapper = TestRenderer.create(<Child />); const wrapper = TestRenderer.create(<Child />);
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType(View);
expect(view.props).toMatchObject({ expect(view.props).toMatchObject({
style: [{}], style: [{}],
first: 'first', first: 'first',
second: 'second' second: 'second',
}); });
}); });
it('should pass through children as a normal prop', () => { it('should pass through children as a normal prop', () => {
const Comp = styled.Text.attrs(() => ({ const Comp = styled.Text.attrs(() => ({
children: 'Probably a bad idea' children: 'Probably a bad idea',
}))``; }))``;
const wrapper = TestRenderer.create(<Comp />); const wrapper = TestRenderer.create(<Comp />);
const text = wrapper.root.findByType('Text'); const text = wrapper.root.findByType(Text);
expect(text.props).toMatchObject({ expect(text.props).toMatchObject({
children: 'Probably a bad idea', children: 'Probably a bad idea',
style: [{}] style: [{}],
}); });
}); });
it('should pass through complex children as well', () => { it('should pass through complex children as well', () => {
const child = <Text>Probably a bad idea</Text>; const child = <Text>Probably a bad idea</Text>;
const Comp = styled.Text.attrs(() => ({ const Comp = styled.Text.attrs(() => ({
children: child children: child,
}))``; }))``;
const wrapper = TestRenderer.create(<Comp />); const wrapper = TestRenderer.create(<Comp />);
const text = wrapper.root.findByType('Text'); const text = wrapper.root.findByType(Text);
expect(text.props).toMatchObject({ expect(text.props).toMatchObject({
children: child, children: child,
style: [{}] style: [{}],
}); });
}); });
it('should override children', () => { it('should override children', () => {
const child = <Text>Amazing</Text>; const child = <Text>Amazing</Text>;
const Comp = styled.Text.attrs(() => ({ const Comp = styled.Text.attrs(() => ({
children: child children: child,
}))``; }))``;
const wrapper = TestRenderer.create(<Comp>Something else</Comp>); const wrapper = TestRenderer.create(<Comp>Something else</Comp>);
const text = wrapper.root.findByType('Text'); const text = wrapper.root.findByType(Text);
expect(text.props).toMatchObject({ expect(text.props).toMatchObject({
children: child, children: child,
style: [{}] style: [{}],
}); });
}); });
it('accepts a function', () => { it('accepts a function', () => {
const child = <Text>Amazing</Text>; const child = <Text>Amazing</Text>;
const Comp = styled.Text.attrs(() => ({ const Comp = styled.Text.attrs(() => ({
children: child children: child,
}))``; }))``;
const wrapper = TestRenderer.create(<Comp>Something else</Comp>); const wrapper = TestRenderer.create(<Comp>Something else</Comp>);
const text = wrapper.root.findByType('Text'); const text = wrapper.root.findByType(Text);
expect(text.props).toMatchObject({ expect(text.props).toMatchObject({
children: child, children: child,
style: [{}] style: [{}],
}); });
}); });
it('function form allows access to theme', () => { it('function form allows access to theme', () => {
const Comp = styled.Text.attrs(props => ({ const Comp = styled.Text.attrs(props => ({
'data-color': props.theme.color 'data-color': props.theme.color,
}))``; }))``;
const wrapper = TestRenderer.create(<ThemeProvider theme={{ color: 'red' }}> const wrapper = TestRenderer.create(
<ThemeProvider theme={{ color: 'red' }}>
<Comp>Something else</Comp> <Comp>Something else</Comp>
</ThemeProvider>); </ThemeProvider>
const text = wrapper.root.findByType('Text'); );
const text = wrapper.root.findByType(Text);
expect(text.props).toMatchObject({ expect(text.props).toMatchObject({
children: 'Something else', children: 'Something else',
'data-color': 'red', 'data-color': 'red',
style: [{}] style: [{}],
}); });
}); });
it('theme prop works', () => { it('theme prop works', () => {
const Comp = styled.Text` const Comp = styled.Text`
color: ${({ color: ${({ theme }) => theme.myColor};
theme
}) => theme.myColor};
`; `;
const wrapper = TestRenderer.create(<Comp theme={{ myColor: 'red' }}>Something else</Comp>); const wrapper = TestRenderer.create(<Comp theme={{ myColor: 'red' }}>Something else</Comp>);
const text = wrapper.root.findByType('Text'); const text = wrapper.root.findByType(Text);
expect(text.props.style).toMatchObject([{ color: 'red' }]); expect(text.props.style).toMatchObject([{ color: 'red' }]);
}); });
it('theme in defaultProps works', () => { it('theme in defaultProps works', () => {
const Comp = styled.Text` const Comp = styled.Text`
color: ${({ color: ${({ theme }) => theme.myColor};
theme
}) => theme.myColor};
`; `;
Comp.defaultProps = { theme: { myColor: 'red' } }; Comp.defaultProps = { theme: { myColor: 'red' } };
const wrapper = TestRenderer.create(<Comp>Something else</Comp>); const wrapper = TestRenderer.create(<Comp>Something else</Comp>);
const text = wrapper.root.findByType('Text'); const text = wrapper.root.findByType(Text);
expect(text.props.style).toMatchObject([{ color: 'red' }]); expect(text.props.style).toMatchObject([{ color: 'red' }]);
}); });
@@ -375,11 +372,12 @@ Object {
describe('expanded API', () => { describe('expanded API', () => {
it('should attach a displayName', () => { it('should attach a displayName', () => {
View.displayName = 'View'; const Dummy = (props: PropsWithChildren<{}>) => <View {...props} />;
Dummy.displayName = 'Dummy';
const Comp = styled(View)``; const Comp = styled(Dummy)``;
expect(Comp.displayName).toBe('Styled(View)'); expect(Comp.displayName).toBe('Styled(Dummy)');
const CompTwo = styled.View.withConfig({ displayName: 'Test' })``; const CompTwo = styled.View.withConfig({ displayName: 'Test' })``;
expect(CompTwo.displayName).toBe('Test'); expect(CompTwo.displayName).toBe('Test');
@@ -387,38 +385,53 @@ Object {
it('should allow multiple calls to be chained', () => { it('should allow multiple calls to be chained', () => {
const Comp = styled.View.withConfig({ displayName: 'Test1' }).withConfig({ const Comp = styled.View.withConfig({ displayName: 'Test1' }).withConfig({
displayName: 'Test2' displayName: 'Test2',
})``; })``;
expect(Comp.displayName).toBe('Test2'); expect(Comp.displayName).toBe('Test2');
}); });
it('withComponent should work', () => { it('withComponent should work', () => {
const Dummy = props => <View {...props} />; const Dummy = (props: PropsWithChildren<{}>) => <View {...props} />;
const Comp = styled.View.withConfig({ const Comp = styled.View.withConfig({
displayName: 'Comp', displayName: 'Comp',
componentId: 'OMGLOL'
})``.withComponent(Text); })``.withComponent(Text);
const Comp2 = styled.View.withConfig({ const Comp2 = styled.View.withConfig({
displayName: 'Comp2', displayName: 'Comp2',
componentId: 'OMFG'
})``.withComponent(Dummy); })``.withComponent(Dummy);
expect(TestRenderer.create(<Comp />).toJSON()).toMatchSnapshot(); expect(TestRenderer.create(<Comp />).toJSON()).toMatchInlineSnapshot(`
expect(TestRenderer.create(<Comp2 />).toJSON()).toMatchSnapshot(); <Text
style={
Array [
Object {},
]
}
/>
`);
expect(TestRenderer.create(<Comp2 />).toJSON()).toMatchInlineSnapshot(`
<View
style={
Array [
Object {},
]
}
/>
`);
}); });
it('"as" prop should change the rendered element without affecting the styling', () => { it('"as" prop should change the rendered element without affecting the styling', () => {
const OtherText = props => <Text {...props} foo />; // @ts-expect-error foo is expected later in the test
const OtherText = (props: PropsWithChildren<{}>) => <Text {...props} foo />;
const Comp = styled.Text` const Comp = styled.Text`
color: red; color: red;
`; `;
const wrapper = TestRenderer.create(<Comp as={OtherText} />); const wrapper = TestRenderer.create(<Comp as={OtherText} />);
const view = wrapper.root.findByType('Text'); const view = wrapper.root.findByType(Text);
expect(view.props).toHaveProperty('foo'); expect(view.props).toHaveProperty('foo');
expect(view.props.style).toEqual([{ color: 'red' }]); expect(view.props.style).toEqual([{ color: 'red' }]);
@@ -429,35 +442,43 @@ Object {
color: ${p => p.$color}; color: ${p => p.$color};
`; `;
expect(TestRenderer.create(<Comp $color="red" />).toJSON()).toMatchSnapshot(); expect(TestRenderer.create(<Comp $color="red" />).toJSON()).toMatchInlineSnapshot(`
<Text
style={
Array [
Object {
"color": "red",
},
]
}
/>
`);
}); });
it('allows for custom prop filtering for elements', () => { it('allows for custom prop filtering for elements', () => {
const Comp = styled('View').withConfig({ const Comp = styled('View').withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop) shouldForwardProp: prop => !['filterThis'].includes(prop),
})` })`
color: red; color: red;
`; `;
const wrapper = TestRenderer.create(<Comp filterThis="abc" passThru="def" />); const wrapper = TestRenderer.create(<Comp filterThis="abc" passThru="def" />);
const { // @ts-expect-error bs error
props const { props } = wrapper.root.findByType('View');
} = wrapper.root.findByType('View');
expect(props.style).toEqual([{ color: 'red' }]); expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def'); expect(props.passThru).toBe('def');
expect(props.filterThis).toBeUndefined(); expect(props.filterThis).toBeUndefined();
}); });
it('allows custom prop filtering for components', () => { it('allows custom prop filtering for components', () => {
const InnerComp = props => <View {...props} />; const InnerComp = (props: PropsWithChildren<{}>) => <View {...props} />;
const Comp = styled(InnerComp).withConfig({ const Comp = styled(InnerComp).withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop) shouldForwardProp: prop => !['filterThis'].includes(prop),
})` })`
color: red; color: red;
`; `;
const wrapper = TestRenderer.create(<Comp filterThis="abc" passThru="def" />); const wrapper = TestRenderer.create(<Comp filterThis="abc" passThru="def" />);
const { // @ts-expect-error bs error
props const { props } = wrapper.root.findByType('View');
} = wrapper.root.findByType('View');
expect(props.style).toEqual([{ color: 'red' }]); expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def'); expect(props.passThru).toBe('def');
expect(props.filterThis).toBeUndefined(); expect(props.filterThis).toBeUndefined();
@@ -465,32 +486,28 @@ Object {
it('composes shouldForwardProp on composed styled components', () => { it('composes shouldForwardProp on composed styled components', () => {
const StyledView = styled.View.withConfig({ const StyledView = styled.View.withConfig({
shouldForwardProp: prop => prop === 'passThru' shouldForwardProp: prop => prop === 'passThru',
})` })`
color: red; color: red;
`; `;
const ComposedView = styled(StyledView).withConfig({ const ComposedView = styled(StyledView).withConfig({
shouldForwardProp: () => true shouldForwardProp: () => true,
})``; })``;
const wrapper = TestRenderer.create(<ComposedView filterThis passThru />); const wrapper = TestRenderer.create(<ComposedView filterThis passThru />);
const { const { props } = wrapper.root.findByType(View);
props
} = wrapper.root.findByType('View');
expect(props.passThru).toBeDefined(); expect(props.passThru).toBeDefined();
expect(props.filterThis).toBeUndefined(); expect(props.filterThis).toBeUndefined();
}); });
it('should filter out props when using "as" to a custom component', () => { it('should filter out props when using "as" to a custom component', () => {
const AsComp = props => <View {...props} />; const AsComp = (props: PropsWithChildren<{}>) => <View {...props} />;
const Comp = styled.View.withConfig({ const Comp = styled.View.withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop) shouldForwardProp: prop => !['filterThis'].includes(prop),
})` })`
color: red; color: red;
`; `;
const wrapper = TestRenderer.create(<Comp as={AsComp} filterThis="abc" passThru="def" />); const wrapper = TestRenderer.create(<Comp as={AsComp} filterThis="abc" passThru="def" />);
const { const { props } = wrapper.root.findByType(AsComp);
props
} = wrapper.root.findByType(AsComp);
expect(props.style).toEqual([{ color: 'red' }]); expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def'); expect(props.passThru).toBe('def');
@@ -498,16 +515,14 @@ Object {
}); });
it('can set computed styles based on props that are being filtered out', () => { it('can set computed styles based on props that are being filtered out', () => {
const AsComp = props => <View {...props} />; const AsComp = (props: PropsWithChildren<{}>) => <View {...props} />;
const Comp = styled.View.withConfig({ const Comp = styled.View.withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop) shouldForwardProp: prop => !['filterThis'].includes(prop),
})` })`
color: ${props => props.filterThis === 'abc' ? 'red' : undefined}; color: ${props => (props.filterThis === 'abc' ? 'red' : undefined)};
`; `;
const wrapper = TestRenderer.create(<Comp as={AsComp} filterThis="abc" passThru="def" />); const wrapper = TestRenderer.create(<Comp as={AsComp} filterThis="abc" passThru="def" />);
const { const { props } = wrapper.root.findByType(AsComp);
props
} = wrapper.root.findByType(AsComp);
expect(props.style).toEqual([{ color: 'red' }]); expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def'); expect(props.passThru).toBe('def');
@@ -516,14 +531,12 @@ Object {
it('should filter our props when using "as" to a different element', () => { it('should filter our props when using "as" to a different element', () => {
const Comp = styled.View.withConfig({ const Comp = styled.View.withConfig({
shouldForwardProp: prop => !['filterThis'].includes(prop) shouldForwardProp: prop => !['filterThis'].includes(prop),
})` })`
color: red; color: red;
`; `;
const wrapper = TestRenderer.create(<Comp as="a" filterThis="abc" passThru="def" />); const wrapper = TestRenderer.create(<Comp as="a" filterThis="abc" passThru="def" />);
const { const { props } = wrapper.root.findByType('a');
props
} = wrapper.root.findByType("a");
expect(props.style).toEqual([{ color: 'red' }]); expect(props.style).toEqual([{ color: 'red' }]);
expect(props.passThru).toBe('def'); expect(props.passThru).toBe('def');
@@ -531,17 +544,18 @@ Object {
}); });
it('should prefer transient $as over as', () => { it('should prefer transient $as over as', () => {
const OtherText = props => <Text {...props} foo />; const OtherText = (props: PropsWithChildren<{}>) => <Text {...props} />;
const Comp = styled.Text` const Comp = styled.Text`
color: red; color: red;
`; `;
const wrapper = TestRenderer.create(<Comp $as="View" as={OtherText} />); const wrapper = TestRenderer.create(<Comp $as="View" as={OtherText} />);
// @ts-expect-error bs error
const view = wrapper.root.findByType('View'); const view = wrapper.root.findByType('View');
expect(view.props.style).toEqual([{ color: 'red' }]); expect(view.props.style).toEqual([{ color: 'red' }]);
expect(() => wrapper.root.findByType('Text')).toThrowError(); expect(() => wrapper.root.findByType(Text)).toThrowError();
}); });
}); });
}); });

View File

@@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`primitives expanded API withComponent should work 1`] = `
<Text
style={
Array [
Object {},
]
}
/>
`;
exports[`primitives expanded API withComponent should work 2`] = `
<View
style={
Array [
Object {},
]
}
/>
`;

View File

@@ -12,8 +12,12 @@ export type ExtensibleObject = BaseExtensibleObject & {
theme?: Theme; theme?: Theme;
}; };
export type ExecutionContext = BaseExtensibleObject & {
theme: Theme;
};
export type StyleFunction<Props> = ( export type StyleFunction<Props> = (
executionContext: Props & ExtensibleObject executionContext: ExecutionContext & Props
) => BaseExtensibleObject | string | number; ) => BaseExtensibleObject | string | number;
// Do not add IStyledComponent to this union, it breaks prop function interpolation in TS // Do not add IStyledComponent to this union, it breaks prop function interpolation in TS
@@ -26,7 +30,7 @@ export type Interpolation<Props extends Object = ExtensibleObject> =
export type Attrs<Props = ExtensibleObject> = export type Attrs<Props = ExtensibleObject> =
| ExtensibleObject | ExtensibleObject
| ((props: ExtensibleObject & Props) => ExtensibleObject); | ((props: ExecutionContext & Props) => ExtensibleObject);
export type RuleSet = Interpolation[]; export type RuleSet = Interpolation[];
export type Styles = string[] | Object | ((executionContext: ExtensibleObject) => Interpolation); export type Styles = string[] | Object | ((executionContext: ExtensibleObject) => Interpolation);
@@ -113,9 +117,7 @@ export type IStyledNativeComponentFactory = (
target: IStyledNativeComponent['target'], target: IStyledNativeComponent['target'],
options: { options: {
attrs?: Attrs[]; attrs?: Attrs[];
componentId: string;
displayName?: string; displayName?: string;
parentComponentId?: string;
shouldForwardProp?: ShouldForwardProp; shouldForwardProp?: ShouldForwardProp;
}, },
rules: RuleSet rules: RuleSet

View File

@@ -1,10 +1,11 @@
// eslint-disable-next-line
const baseConfig = require('./config.base'); const baseConfig = require('./config.base');
module.exports = Object.assign({}, baseConfig, { module.exports = Object.assign({}, baseConfig, {
preset: 'react-native', preset: 'react-native',
setupFiles: ['<rootDir>/packages/styled-components/src/test/globals.js'], setupFiles: ['<rootDir>/packages/styled-components/src/test/globals.ts'],
testEnvironment: 'node', testEnvironment: 'node',
testRegex: 'src/native/test/.*.js$', testRegex: 'src/native/test/.*.tsx?$',
transform: { transform: {
'^.+\\.js$': '<rootDir>/node_modules/react-native/jest/preprocessor.js', '^.+\\.js$': '<rootDir>/node_modules/react-native/jest/preprocessor.js',
}, },