Skip to content

Commit

Permalink
[change] Text uses createContext
Browse files Browse the repository at this point in the history
Replaces the legacy 'contextTypes' API.
Removes 'enzyme' from related tests. Back to recording DOM snapshots.

Close #1248
Ref #1172
  • Loading branch information
necolas committed Aug 9, 2019
1 parent 1966844 commit 5b1ab49
Show file tree
Hide file tree
Showing 13 changed files with 723 additions and 210 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@babel/preset-env": "^7.2.3",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@testing-library/react": "^8.0.5",
"babel-eslint": "^10.0.0",
"babel-jest": "24.0.0-alpha.9",
"babel-loader": "^8.0.0",
Expand Down

Large diffs are not rendered by default.

143 changes: 54 additions & 89 deletions packages/react-native-web/src/exports/Image/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import Image from '../';
import ImageLoader from '../../../modules/ImageLoader';
import ImageUriCache from '../ImageUriCache';
import React from 'react';
import { shallow } from 'enzyme';
import StyleSheet from '../../StyleSheet';
import { render } from '@testing-library/react';

const originalImage = window.Image;

const findImageSurfaceStyle = wrapper => StyleSheet.flatten(wrapper.childAt(0).prop('style'));

describe('components/Image', () => {
beforeEach(() => {
ImageUriCache._entries = {};
Expand All @@ -24,37 +21,35 @@ describe('components/Image', () => {

test('prop "accessibilityLabel"', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const component = shallow(
const { container } = render(
<Image accessibilityLabel="accessibilityLabel" defaultSource={defaultSource} />
);
const img = component.find('img');
expect(component.prop('accessibilityLabel')).toBe('accessibilityLabel');
expect(img.prop('alt')).toBe('accessibilityLabel');
expect(container.firstChild).toMatchSnapshot();
});

test('prop "accessible"', () => {
const component = shallow(<Image accessible={false} />);
expect(component.prop('accessible')).toBe(false);
const { container } = render(<Image accessible={false} />);
expect(container.firstChild).toMatchSnapshot();
});

test('prop "blurRadius"', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const component = shallow(<Image blurRadius={5} defaultSource={defaultSource} />);
expect(findImageSurfaceStyle(component).filter).toMatchSnapshot();
const { container } = render(<Image blurRadius={5} defaultSource={defaultSource} />);
expect(container.firstChild).toMatchSnapshot();
});

describe('prop "defaultSource"', () => {
test('sets background image when value is an object', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const component = shallow(<Image defaultSource={defaultSource} />);
expect(findImageSurfaceStyle(component).backgroundImage).toMatchSnapshot();
const { container } = render(<Image defaultSource={defaultSource} />);
expect(container.firstChild).toMatchSnapshot();
});

test('sets background image when value is a string', () => {
// emulate require-ed asset
const defaultSource = 'https://google.com/favicon.ico';
const component = shallow(<Image defaultSource={defaultSource} />);
expect(findImageSurfaceStyle(component).backgroundImage).toMatchSnapshot();
const { container } = render(<Image defaultSource={defaultSource} />);
expect(container.firstChild).toMatchSnapshot();
});

test('sets "height" and "width" styles if missing', () => {
Expand All @@ -63,10 +58,8 @@ describe('components/Image', () => {
height: 10,
width: 20
};
const component = shallow(<Image defaultSource={defaultSource} />);
const { height, width } = StyleSheet.flatten(component.prop('style'));
expect(height).toBe(10);
expect(width).toBe(20);
const { container } = render(<Image defaultSource={defaultSource} />);
expect(container.firstChild).toMatchSnapshot();
});

test('does not override "height" and "width" styles', () => {
Expand All @@ -75,21 +68,17 @@ describe('components/Image', () => {
height: 10,
width: 20
};
const component = shallow(
const { container } = render(
<Image defaultSource={defaultSource} style={{ height: 20, width: 40 }} />
);
const { height, width } = StyleSheet.flatten(component.prop('style'));
expect(height).toBe(20);
expect(width).toBe(40);
expect(container.firstChild).toMatchSnapshot();
});
});

test('prop "draggable"', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const component = shallow(<Image defaultSource={defaultSource} />);
expect(component.find('img').prop('draggable')).toBe(false);
component.setProps({ defaultSource, draggable: true });
expect(component.find('img').prop('draggable')).toBe(true);
const { container } = render(<Image defaultSource={defaultSource} draggable={true} />);
expect(container.firstChild).toMatchSnapshot();
});

describe('prop "onLoad"', () => {
Expand All @@ -99,7 +88,7 @@ describe('components/Image', () => {
onLoad();
});
const onLoadStub = jest.fn();
shallow(<Image onLoad={onLoadStub} source="https://test.com/img.jpg" />);
render(<Image onLoad={onLoadStub} source="https://test.com/img.jpg" />);
jest.runOnlyPendingTimers();
expect(ImageLoader.load).toBeCalled();
expect(onLoadStub).toBeCalled();
Expand All @@ -113,7 +102,7 @@ describe('components/Image', () => {
const onLoadStub = jest.fn();
const uri = 'https://test.com/img.jpg';
ImageUriCache.add(uri);
shallow(<Image onLoad={onLoadStub} source={uri} />);
render(<Image onLoad={onLoadStub} source={uri} />);
jest.runOnlyPendingTimers();
expect(ImageLoader.load).not.toBeCalled();
expect(onLoadStub).toBeCalled();
Expand All @@ -122,26 +111,28 @@ describe('components/Image', () => {

test('is called on update if "uri" is different', () => {
const onLoadStub = jest.fn();
const uri = 'https://test.com/img.jpg';
const component = shallow(<Image onLoad={onLoadStub} source={uri} />);
component.setProps({ source: 'https://blah.com/img.png' });
const { rerender } = render(
<Image onLoad={onLoadStub} source={'https://test.com/img.jpg'} />
);
rerender(<Image onLoad={onLoadStub} source={'https://blah.com/img.png'} />);
expect(onLoadStub.mock.calls.length).toBe(2);
});

test('is not called on update if "uri" is the same', () => {
const onLoadStub = jest.fn();
const uri = 'https://test.com/img.jpg';
const component = shallow(<Image onLoad={onLoadStub} source={uri} />);
component.setProps({ resizeMode: 'stretch' });
const { rerender } = render(
<Image onLoad={onLoadStub} source={'https://test.com/img.jpg'} />
);
rerender(<Image onLoad={onLoadStub} source={'https://test.com/img.jpg'} />);
expect(onLoadStub.mock.calls.length).toBe(1);
});
});

describe('prop "resizeMode"', () => {
['contain', 'cover', 'none', 'repeat', 'stretch', undefined].forEach(resizeMode => {
test(`value "${resizeMode}"`, () => {
const component = shallow(<Image resizeMode={resizeMode} />);
expect(findImageSurfaceStyle(component).backgroundSize).toMatchSnapshot();
const { container } = render(<Image resizeMode={resizeMode} />);
expect(container.firstChild).toMatchSnapshot();
});
});
});
Expand All @@ -150,15 +141,15 @@ describe('components/Image', () => {
test('does not throw', () => {
const sources = [null, '', {}, { uri: '' }, { uri: 'https://google.com' }];
sources.forEach(source => {
expect(() => shallow(<Image source={source} />)).not.toThrow();
expect(() => render(<Image source={source} />)).not.toThrow();
});
});

test('is not set immediately if the image has not already been loaded', () => {
const uri = 'https://google.com/favicon.ico';
const source = { uri };
const component = shallow(<Image source={source} />);
expect(component.find('img')).toBeUndefined;
const { container } = render(<Image source={source} />);
expect(container.firstChild).toMatchSnapshot();
});

test('is set immediately if the image was preloaded', () => {
Expand All @@ -168,8 +159,8 @@ describe('components/Image', () => {
});
return Image.prefetch(uri).then(() => {
const source = { uri };
const component = shallow(<Image source={source} />, { disableLifecycleMethods: true });
expect(component.find('img').prop('src')).toBe(uri);
const { container } = render(<Image source={source} />, { disableLifecycleMethods: true });
expect(container.firstChild).toMatchSnapshot();
ImageUriCache.remove(uri);
});
});
Expand All @@ -181,36 +172,20 @@ describe('components/Image', () => {
ImageUriCache.add(uriTwo);

// initial render
const component = shallow(<Image source={{ uri: uriOne }} />);
const { container, rerender } = render(<Image source={{ uri: uriOne }} />);
ImageUriCache.remove(uriOne);
expect(
component
.render()
.find('img')
.attr('src')
).toBe(uriOne);

expect(container.firstChild).toMatchSnapshot();
// props update
component.setProps({ source: { uri: uriTwo } });
rerender(<Image source={{ uri: uriTwo }} />);
ImageUriCache.remove(uriTwo);
expect(
component
.render()
.find('img')
.attr('src')
).toBe(uriTwo);
expect(container.firstChild).toMatchSnapshot();
});

test('is correctly updated when missing in initial render', () => {
const uri = 'https://testing.com/img.jpg';
const component = shallow(<Image />);
component.setProps({ source: { uri } });
expect(
component
.render()
.find('img')
.attr('src')
).toBe(uri);
const { container, rerender } = render(<Image />);
rerender(<Image source={{ uri }} />);
expect(container.firstChild).toMatchSnapshot();
});

test('is correctly updated only when loaded if defaultSource provided', () => {
Expand All @@ -220,53 +195,43 @@ describe('components/Image', () => {
ImageLoader.load = jest.fn().mockImplementationOnce((_, onLoad, onError) => {
loadCallback = onLoad;
});
const component = shallow(<Image defaultSource={{ uri: defaultUri }} source={{ uri }} />);
expect(component.find('img').prop('src')).toBe(defaultUri);
const { container } = render(<Image defaultSource={{ uri: defaultUri }} source={{ uri }} />);
expect(container.firstChild).toMatchSnapshot();
loadCallback();
expect(component.find('img').prop('src')).toBe(uri);
expect(container.firstChild).toMatchSnapshot();
});
});

describe('prop "style"', () => {
test('supports "resizeMode" property', () => {
const component = shallow(<Image style={{ resizeMode: 'contain' }} />);
expect(findImageSurfaceStyle(component).backgroundSize).toMatchSnapshot();
const { container } = render(<Image style={{ resizeMode: 'contain' }} />);
expect(container.firstChild).toMatchSnapshot();
});

test('supports "shadow" properties (convert to filter)', () => {
const component = shallow(
const { container } = render(
<Image style={{ shadowColor: 'red', shadowOffset: { width: 1, height: 1 } }} />
);
expect(findImageSurfaceStyle(component).filter).toMatchSnapshot();
expect(container.firstChild).toMatchSnapshot();
});

test('supports "tintcolor" property (convert to filter)', () => {
const defaultSource = { uri: 'https://google.com/favicon.ico' };
const component = shallow(
const { container } = render(
<Image defaultSource={defaultSource} style={{ tintColor: 'red' }} />
);
// filter
expect(findImageSurfaceStyle(component).filter).toContain('url(#tint-');
// svg
expect(component.childAt(2).type()).toBe('svg');
expect(container.firstChild).toMatchSnapshot();
});

test('removes other unsupported View styles', () => {
const component = shallow(<Image style={{ overlayColor: 'red', tintColor: 'blue' }} />);
expect(component.props().style.overlayColor).toBeUndefined();
expect(component.props().style.tintColor).toBeUndefined();
const { container } = render(<Image style={{ overlayColor: 'red', tintColor: 'blue' }} />);
expect(container.firstChild).toMatchSnapshot();
});
});

test('prop "testID"', () => {
const component = shallow(<Image testID="testID" />);
expect(component.prop('testID')).toBe('testID');
});

test('passes other props through to underlying View', () => {
const fn = () => {};
const component = shallow(<Image onResponderGrant={fn} />);
expect(component.prop('onResponderGrant')).toBe(fn);
const { container } = render(<Image testID="testID" />);
expect(container.firstChild).toMatchSnapshot();
});

test('queryCache', () => {
Expand Down
22 changes: 11 additions & 11 deletions packages/react-native-web/src/exports/Image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ImageStylePropTypes from './ImageStylePropTypes';
import ImageUriCache from './ImageUriCache';
import StyleSheet from '../StyleSheet';
import StyleSheetPropType from '../../modules/StyleSheetPropType';
import TextAncestorContext from '../Text/TextAncestorContext';
import View from '../View';
import ViewPropTypes from '../ViewPropTypes';
import { bool, func, number, oneOf, shape } from 'prop-types';
Expand Down Expand Up @@ -97,10 +98,6 @@ type State = {
class Image extends Component<*, State> {
static displayName = 'Image';

static contextTypes = {
isInAParentText: bool
};

static propTypes = {
...ViewPropTypes,
blurRadius: number,
Expand Down Expand Up @@ -198,7 +195,7 @@ class Image extends Component<*, State> {
this._isMounted = false;
}

render() {
renderImage(hasTextAncestor) {
const { shouldDisplaySource } = this.state;
const {
accessibilityLabel,
Expand Down Expand Up @@ -286,12 +283,7 @@ class Image extends Component<*, State> {
accessibilityLabel={accessibilityLabel}
accessible={accessible}
onLayout={this._createLayoutHandler(finalResizeMode)}
style={[
styles.root,
this.context.isInAParentText && styles.inline,
imageSizeStyle,
flatStyle
]}
style={[styles.root, hasTextAncestor && styles.inline, imageSizeStyle, flatStyle]}
testID={testID}
>
<View
Expand All @@ -309,6 +301,14 @@ class Image extends Component<*, State> {
);
}

render() {
return (
<TextAncestorContext.Consumer>
{hasTextAncestor => this.renderImage(hasTextAncestor)}
</TextAncestorContext.Consumer>
);
}

_createImageLoader() {
const { source } = this.props;
this._destroyImageLoader();
Expand Down
12 changes: 12 additions & 0 deletions packages/react-native-web/src/exports/Text/TextAncestorContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/

import * as React from 'react';
const TextAncestorContext = React.createContext(false);
export default (TextAncestorContext: React.Context<boolean>);
Loading

0 comments on commit 5b1ab49

Please sign in to comment.