* Copyright (c) 2024 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE-MIT file in the root directory of this source tree.
*/
import { Image, ImageSourcePropType, ScrollView, Text, View } from 'react-native';
import { TestCase, TestSuite } from '@rnoh/testerino';
import React from 'react';
import { Button } from '../components';
const WRONG_IMAGE_SRC = 'not_image';
const LOCAL_IMAGE_ASSET_ID = require('../assets/pravatar-131.jpg');
const REMOTE_IMAGE_URL = 'https://i.pravatar.cc/100?img=31';
const LARGE_REMOTE_IMAGE_URL =
'https://images.unsplash.com/photo-1556740749-887f6717d7e4';
const REMOTE_REDIRECT_IMAGE_URL = 'http://placeholder.com/350x150';
const REMOTE_GIF_URL =
'https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif';
const DATA_URI =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
export const ImageTest = () => {
return (
<TestSuite name="Image">
<TestCase itShould="support loading local images">
<Image
style={{ borderRadius: 8, borderWidth: 1 }}
source={LOCAL_IMAGE_ASSET_ID}
/>
</TestCase>
<ImageExampleCase
itShould="support loading remote images"
source={{ uri: REMOTE_IMAGE_URL }}
/>
<ImageExampleCase
itShould="support loading remote images (with http redirect)"
source={{ uri: REMOTE_REDIRECT_IMAGE_URL }}
/>
<ImageExampleCase
itShould="support loading large remote images (over 5mb)"
source={{ uri: LARGE_REMOTE_IMAGE_URL }}
/>
<ImageExampleCase
itShould="support loading image data uris"
source={{ uri: DATA_URI }}
/>
<ImageExampleCase
itShould="support loading remote animated gifs"
source={{ uri: REMOTE_GIF_URL }}
/>
<TestCase itShould="display alt when the image doesn't load">
<View>
<Image
source={require('../assets/fonts/Pacifico-Regular.ttf')}
alt="This image could not be loaded!"
/>
</View>
</TestCase>
<TestCase
itShould="retrieve remote image size"
fn={({ expect }) => {
return new Promise((resolve, reject) => {
Image.getSize(
REMOTE_IMAGE_URL,
(width, height) => {
expect(width).to.be.eq(100);
expect(height).to.be.eq(100);
resolve();
},
e => {
reject(e);
},
);
});
}}
/>
<TestCase
itShould="retrieve local image size"
fn={({ expect }) => {
const resolvedAsset = Image.resolveAssetSource(LOCAL_IMAGE_ASSET_ID);
expect(resolvedAsset.width).to.be.eq(150);
expect(resolvedAsset.height).to.be.eq(150);
}}
/>
<TestCase
itShould="prefetch image"
fn={async ({ expect }) => {
let ex: any;
try {
await Image.prefetch(WRONG_IMAGE_SRC);
} catch (e) {
ex = e;
}
expect(ex).to.be.not.undefined;
expect(await Image.prefetch(REMOTE_IMAGE_URL)).to.be.true;
expect(await Image.prefetch(REMOTE_IMAGE_URL)).to.be.true;
}}
/>
<TestCase
itShould="query cache"
fn={async ({ expect }) => {
await Image.prefetch(REMOTE_IMAGE_URL);
expect(Image.queryCache).not.to.be.undefined;
const result = await Image.queryCache?.([
REMOTE_IMAGE_URL,
WRONG_IMAGE_SRC,
]);
expect(result).to.be.not.undefined;
expect(result?.[REMOTE_IMAGE_URL]).to.be.not.undefined;
expect(result?.[REMOTE_IMAGE_URL]).to.be.eq('disk');
expect(result?.[WRONG_IMAGE_SRC]).to.be.undefined;
}}
/>
<TestCase
skip // https://gl.swmansion.com/rnoh/react-native-harmony/-/issues/246
itShould="render circular image on a red rectangle (overlayColor)">
<Image
source={LOCAL_IMAGE_ASSET_ID}
style={{ overlayColor: 'red', borderRadius: Number.MAX_SAFE_INTEGER }}
/>
</TestCase>
<TestCase
itShould="call onLoadStart"
initialState={'not called'}
arrange={({ setState }) => {
return (
<Image
source={LOCAL_IMAGE_ASSET_ID}
onLoadStart={() => setState('called')}
/>
);
}}
assert={({ expect, state }) => {
expect(state).to.be.eq('called');
}}
/>
<TestCase
itShould="call onLoad"
initialState={{}}
arrange={({ setState, state }) => {
return (
<View>
<Text>{JSON.stringify(state)}</Text>
<Image
source={LOCAL_IMAGE_ASSET_ID}
onLoad={event => {
setState(event.nativeEvent.source);
}}
/>
</View>
);
}}
assert={({ expect, state }) => {
expect(state).to.contain.all.keys('width', 'height', 'uri');
}}
/>
<TestCase
itShould="call onError (local)"
initialState={null}
arrange={({ setState, state }) => {
return (
<View>
<Text>{JSON.stringify(state)}</Text>
<Image
source={require('../assets/fonts/Pacifico-Regular.ttf')}
onError={event => {
setState(event.nativeEvent.error);
}}
/>
</View>
);
}}
assert={({ expect, state }) => {
expect(state).to.be.not.null;
}}
/>
<TestCase
itShould="call onError (remote)"
initialState={null}
arrange={({ setState, state }) => {
return (
<View>
<Text>{JSON.stringify(state)}</Text>
<Image
source={{ uri: 'https://www.google.com/image' }}
onError={event => {
setState(event.nativeEvent.error);
}}
/>
</View>
);
}}
assert={({ expect, state }) => {
expect(state).to.be.not.null;
}}
/>
<TestSuite
name="resizeMode" // https://gl.swmansion.com/rnoh/react-native-harmony/-/issues/245
>
<TestCase itShould="render small image in the center (center)">
<Image
style={{ width: '100%', height: 100 }}
source={LOCAL_IMAGE_ASSET_ID}
resizeMode="center"
/>
</TestCase>
<TestCase itShould="render image touching top and bottom edges in the center (contain)">
<Image
style={{ width: '100%', height: 100 }}
source={LOCAL_IMAGE_ASSET_ID}
resizeMode="contain"
/>
</TestCase>
<TestCase itShould="fully cover test case area while preserving aspect ratio (cover)">
<Image
style={{ width: '100%', height: 100 }}
source={LOCAL_IMAGE_ASSET_ID}
resizeMode="cover"
/>
</TestCase>
<TestCase itShould="cover test case area by repeating image (repeat)">
<Image
style={{ width: '100%', height: 100 }}
source={LOCAL_IMAGE_ASSET_ID}
resizeMode="repeat"
/>
</TestCase>
<TestCase itShould="cover test case area by stretching (stretch)">
<Image
style={{ width: '100%', height: 100 }}
source={LOCAL_IMAGE_ASSET_ID}
resizeMode="stretch"
/>
</TestCase>
</TestSuite>
<TestSuite name="blurRadius">
<TestCase itShould="blur images with various blur radius">
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
<Image
style={{ width: 64, height: 64, margin: 4 }}
source={LOCAL_IMAGE_ASSET_ID}
blurRadius={0}
/>
<Image
style={{ width: 64, height: 64, margin: 4 }}
source={LOCAL_IMAGE_ASSET_ID}
blurRadius={5}
/>
<Image
style={{ width: 64, height: 64, margin: 4 }}
source={LOCAL_IMAGE_ASSET_ID}
blurRadius={10}
/>
<Image
style={{ width: 64, height: 64, margin: 4 }}
source={LOCAL_IMAGE_ASSET_ID}
blurRadius={15}
/>
<Image
style={{ width: 64, height: 64, margin: 4 }}
source={LOCAL_IMAGE_ASSET_ID}
blurRadius={20}
/>
<Image
style={{ width: 64, height: 64, margin: 4 }}
source={LOCAL_IMAGE_ASSET_ID}
blurRadius={25}
/>
</View>
</TestCase>
</TestSuite>
<TestCase itShould="replace opaque pixels with the green color (tintColor)">
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-around',
}}>
<Image
source={require('../assets/expo.png')}
style={{
width: 100,
height: 100,
}}
/>
<Image
source={require('../assets/expo.png')}
style={{
width: 100,
height: 100,
tintColor: 'green',
}}
/>
</View>
</TestCase>
<TestCase modal itShould="stop displaying on press">
<SwitchSourceTest />
</TestCase>
<TestCase itShould="render top image in a bit lower quality (difference barely visible)">
<Image
style={{ width: 200, height: 200 }}
source={require('../assets/noise.png')}
resizeMethod="resize"
resizeMode="stretch"
/>
<View style={{ height: 10 }} />
<Image
style={{ width: 200, height: 200 }}
source={require('../assets/noise.png')}
resizeMethod="scale"
resizeMode="stretch"
/>
</TestCase>
<TestCase
modal
skip // https://gl.swmansion.com/rnoh/react-native-harmony/-/issues/483
itShould="fade images with varying durations">
<View style={{ flexDirection: 'row', gap: 24 }}>
<View style={{ width: 100 }}>
<Image
// HACK: ?v=Date.now() is used to prevent caching
// - cached images are not fading/faded in
source={{ uri: REMOTE_IMAGE_URL + '?v=' + Date.now() }}
style={{ width: 100, height: 100, borderRadius: 8 }}
fadeDuration={0}
/>
<Text>This image will fade in over the time of 0s.</Text>
</View>
<View style={{ width: 100 }}>
<Image
source={{ uri: REMOTE_IMAGE_URL + '?v=' + Date.now() }}
style={{ width: 100, height: 100, borderRadius: 8 }}
fadeDuration={1500}
/>
<Text>This image will fade in over the time of 1.5s.</Text>
</View>
<View style={{ width: 100 }}>
<Image
source={{ uri: REMOTE_IMAGE_URL + '?v=' + Date.now() }}
style={{ width: 100, height: 100, borderRadius: 8 }}
fadeDuration={5000}
/>
<Text>This image will fade in over the time of 5s.</Text>
</View>
</View>
{/* To test local fadeDuration for localImages you have to disable caching */}
<View style={{ flexDirection: 'row', gap: 24 }}>
<View style={{ width: 100 }}>
<Image
source={LOCAL_IMAGE_ASSET_ID}
style={{ width: 100, height: 100, borderRadius: 8 }}
fadeDuration={0}
/>
<Text>This image will fade in over the time of 0s.</Text>
</View>
<View style={{ width: 100 }}>
<Image
source={LOCAL_IMAGE_ASSET_ID}
style={{ width: 100, height: 100, borderRadius: 8 }}
fadeDuration={1500}
/>
<Text>This image will fade in over the time of 1.5s.</Text>
</View>
<View style={{ width: 100 }}>
<Image
source={LOCAL_IMAGE_ASSET_ID}
style={{ width: 100, height: 100, borderRadius: 8 }}
fadeDuration={5000}
/>
<Text>This image will fade in over the time of 5s.</Text>
</View>
</View>
</TestCase>
<TestCase
modal
itShould="load many large images without causing out-of-memory issues">
<ScrollView>
{Array.from({ length: 25 }, (_, index) => (
<Image
key={index}
style={{ width: 200, height: 200 }}
source={{ uri: LARGE_REMOTE_IMAGE_URL }}
/>
))}
</ScrollView>
</TestCase>
</TestSuite>
);
};
const ImageExampleCase = ({
itShould,
source,
}: {
itShould: string;
source: ImageSourcePropType;
}) => (
<TestCase itShould={itShould}>
<Image
style={{ borderRadius: 8, borderWidth: 1, height: 150 }}
source={source}
onError={e => console.error(e.nativeEvent.error)}
// resizeMode="contain"
/>
</TestCase>
);
const SwitchSourceTest = () => {
const SOURCES = [
REMOTE_IMAGE_URL,
'',
REMOTE_REDIRECT_IMAGE_URL,
WRONG_IMAGE_SRC,
];
const [idx, setIdx] = React.useState(0);
return (
<View>
<View style={{ flexDirection: 'row' }}>
<Image source={{ uri: SOURCES[idx] }} style={{ width: 100, height: 100 }} />
<Text>{`Source: ${SOURCES[idx]}`}</Text>
</View>
<Button
label="Switch Source"
onPress={() => {
setIdx(i => (i + 1) % SOURCES.length);
}}
/>
</View>
);
};