e2e configuration and example transferred

This commit is contained in:
Bartol Karuza 2019-03-02 17:01:34 +08:00
parent 02cf845006
commit 9f176640d8
7 changed files with 556 additions and 1 deletions

View File

@ -10,6 +10,7 @@
import React, {Component} from 'react';
import {StyleSheet, Text, View} from 'react-native';
import CameraRoll from '@react-native-community/cameraroll';
import CameraRollExample from './js/CameraRollExample';
type Props = {};
export default class App extends Component<Props> {
@ -20,7 +21,7 @@ export default class App extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text>Welcome</Text>
<CameraRollExample />
</View>
);
}

4
example/e2e/config.json Normal file
View File

@ -0,0 +1,4 @@
{
"setupFilesAfterEnv": ["./init.js"],
"testEnvironment": "node"
}

29
example/e2e/init.js Normal file
View File

@ -0,0 +1,29 @@
/**
* 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.
*
* @format
*/
/* eslint-env jest, jasmine */
const detox = require('detox');
const config = require('../../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
jest.setTimeout(300000);
jasmine.getEnv().addReporter(adapter);
beforeAll(async () => {
await detox.init(config, {initGlobals: false, launchApp: false});
});
beforeEach(async () => {
await adapter.beforeEach();
});
afterAll(async () => {
await adapter.afterAll();
await detox.cleanup();
});

View File

@ -0,0 +1,15 @@
const {device, expect, element, by, waitFor} = require('detox');
describe('CameraRoll', () => {
beforeEach(async () => {
await device.launchApp({permissions: {
photos: 'YES',
// camera: 'YES',
}});
});
it('should load example app with no errors and show all the examples by default', async () => {
await expect(element(by.text('Big Images'))).toExist();
});
});

View File

@ -0,0 +1,93 @@
/**
* 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.
*
* @format
* @flow strict-local
*/
'use strict';
const React = require('react');
const ReactNative = require('react-native');
const {Image, StyleSheet, View, ScrollView} = ReactNative;
import type {PhotoIdentifier} from 'CameraRoll';
type Props = $ReadOnly<{|
asset: PhotoIdentifier,
|}>;
type State = {|
asset: PhotoIdentifier,
|};
class AssetScaledImageExample extends React.Component<Props, State> {
state = {
asset: this.props.asset,
};
render() {
const image = this.state.asset.node.image;
return (
<ScrollView>
<View style={styles.row}>
<Image source={image} style={styles.imageWide} />
</View>
<View style={styles.row}>
<Image source={image} style={styles.imageThumb} />
<Image source={image} style={styles.imageThumb} />
<Image source={image} style={styles.imageThumb} />
</View>
<View style={styles.row}>
<Image source={image} style={styles.imageT1} />
<Image source={image} style={styles.imageT2} />
</View>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
row: {
padding: 5,
flex: 1,
flexDirection: 'row',
alignSelf: 'center',
},
imageWide: {
borderWidth: 1,
borderColor: 'black',
width: 320,
height: 240,
margin: 5,
},
imageThumb: {
borderWidth: 1,
borderColor: 'black',
width: 100,
height: 100,
margin: 5,
},
imageT1: {
borderWidth: 1,
borderColor: 'black',
width: 212,
height: 320,
margin: 5,
},
imageT2: {
borderWidth: 1,
borderColor: 'black',
width: 100,
height: 320,
margin: 5,
},
});
exports.title = '<AssetScaledImageExample>';
exports.description =
'Example component that displays the automatic scaling capabilities of the <Image /> tag';
module.exports = AssetScaledImageExample;

View File

@ -0,0 +1,148 @@
/**
* 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
* @format
*/
'use strict';
const React = require('react');
const ReactNative = require('react-native');
const {
CameraRoll,
Image,
Slider,
StyleSheet,
Switch,
Text,
View,
TouchableOpacity,
} = ReactNative;
const invariant = require('invariant');
const CameraRollView = require('./CameraRollView');
const AssetScaledImageExampleView = require('./AssetScaledImageExample');
import type {PhotoIdentifier, GroupTypes} from 'CameraRoll';
type Props = $ReadOnly<{|
navigator?: ?Array<
$ReadOnly<{|
title: string,
component: Class<React.Component<any, any>>,
backButtonTitle: string,
passProps: $ReadOnly<{|asset: PhotoIdentifier|}>,
|}>,
>,
|}>;
type State = {|
groupTypes: GroupTypes,
sliderValue: number,
bigImages: boolean,
|};
export default class CameraRollExample extends React.Component<Props, State> {
state = {
groupTypes: 'SavedPhotos',
sliderValue: 1,
bigImages: true,
};
_cameraRollView: ?React.ElementRef<typeof CameraRollView>;
render() {
return (
<View>
<Switch
onValueChange={this._onSwitchChange}
value={this.state.bigImages}
/>
<Text>{(this.state.bigImages ? 'Big' : 'Small') + ' Images'}</Text>
<Slider
value={this.state.sliderValue}
onValueChange={this._onSliderChange}
/>
<Text>{'Group Type: ' + this.state.groupTypes}</Text>
<CameraRollView
ref={ref => {
this._cameraRollView = ref;
}}
batchSize={20}
groupTypes={this.state.groupTypes}
renderImage={this._renderImage}
bigImages={this.state.bigImages}
/>
</View>
);
}
loadAsset(asset) {
if (this.props.navigator) {
this.props.navigator.push({
title: 'Camera Roll Image',
component: AssetScaledImageExampleView,
backButtonTitle: 'Back',
passProps: {asset: asset},
});
}
}
_renderImage = (asset: PhotoIdentifier) => {
const imageSize = this.state.bigImages ? 150 : 75;
const imageStyle = [styles.image, {width: imageSize, height: imageSize}];
const {location} = asset.node;
const locationStr = location
? JSON.stringify(location)
: 'Unknown location';
return (
<TouchableOpacity
key={asset.node.image.uri}
onPress={this.loadAsset.bind(this, asset)}>
<View style={styles.row}>
<Image source={asset.node.image} style={imageStyle} />
<View style={styles.info}>
<Text style={styles.url}>{asset.node.image.uri}</Text>
<Text>{locationStr}</Text>
<Text>{asset.node.group_name}</Text>
<Text>{new Date(asset.node.timestamp).toString()}</Text>
</View>
</View>
</TouchableOpacity>
);
};
_onSliderChange = value => {
const options = Object.keys(CameraRoll.GroupTypesOptions);
const index = Math.floor(value * options.length * 0.99);
const groupTypes = options[index];
if (groupTypes !== this.state.groupTypes) {
this.setState({groupTypes: groupTypes});
}
};
_onSwitchChange = value => {
invariant(this._cameraRollView, 'ref should be set');
this.setState({bigImages: value});
};
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
flex: 1,
},
url: {
fontSize: 9,
marginBottom: 14,
},
image: {
margin: 4,
},
info: {
flex: 1,
},
});

View File

@ -0,0 +1,265 @@
/**
* 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.
*
* @format
* @flow
*/
'use strict';
const React = require('react');
const ReactNative = require('react-native');
const {
ActivityIndicator,
Alert,
CameraRoll,
Image,
FlatList,
PermissionsAndroid,
Platform,
StyleSheet,
View,
} = ReactNative;
const groupByEveryN = require('groupByEveryN');
const logError = require('logError');
import type {
PhotoIdentifier,
PhotoIdentifiersPage,
GetPhotosParams,
} from 'CameraRoll';
type Props = $ReadOnly<{|
/**
* The group where the photos will be fetched from. Possible
* values are 'Album', 'All', 'Event', 'Faces', 'Library', 'PhotoStream'
* and SavedPhotos.
*/
groupTypes:
| 'Album'
| 'All'
| 'Event'
| 'Faces'
| 'Library'
| 'PhotoStream'
| 'SavedPhotos',
/**
* Number of images that will be fetched in one page.
*/
batchSize: number,
/**
* A function that takes a single image as a parameter and renders it.
*/
renderImage: PhotoIdentifier => React.Node,
/**
* imagesPerRow: Number of images to be shown in each row.
*/
imagesPerRow: number,
/**
* A boolean that indicates if we should render large or small images.
*/
bigImages?: boolean,
/**
* The asset type, one of 'Photos', 'Videos' or 'All'
*/
assetType: 'Photos' | 'Videos' | 'All',
|}>;
type State = {|
assets: Array<PhotoIdentifier>,
data: Array<Array<?PhotoIdentifier>>,
seen: Set<string>,
lastCursor: ?string,
noMore: boolean,
loadingMore: boolean,
|};
type Row = {
item: Array<?PhotoIdentifier>,
};
class CameraRollView extends React.Component<Props, State> {
static defaultProps = {
groupTypes: 'SavedPhotos',
batchSize: 5,
imagesPerRow: 1,
assetType: 'Photos',
renderImage: function(asset: PhotoIdentifier) {
const imageSize = 150;
const imageStyle = [styles.image, {width: imageSize, height: imageSize}];
return <Image source={asset.node.image} style={imageStyle} />;
},
};
state = this.getInitialState();
getInitialState() {
return {
assets: [],
data: [],
seen: new Set(),
lastCursor: null,
noMore: false,
loadingMore: false,
};
}
componentDidMount() {
this.fetch();
}
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.groupTypes !== nextProps.groupTypes) {
this.fetch(true);
}
}
async _fetch(clear?: boolean) {
if (clear) {
this.setState(this.getInitialState(), this.fetch);
return;
}
if (Platform.OS === 'android') {
const result = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
{
title: 'Permission Explanation',
message: 'RNTester would like to access your pictures.',
},
);
if (result !== 'granted') {
Alert.alert('Access to pictures was denied.');
return;
}
}
const fetchParams: GetPhotosParams = {
first: this.props.batchSize,
groupTypes: this.props.groupTypes,
assetType: this.props.assetType,
};
if (Platform.OS === 'android') {
// not supported in android
delete fetchParams.groupTypes;
}
if (this.state.lastCursor) {
fetchParams.after = this.state.lastCursor;
}
try {
const data = await CameraRoll.getPhotos(fetchParams);
this._appendAssets(data);
} catch (e) {
logError(e);
}
}
/**
* Fetches more images from the camera roll. If clear is set to true, it will
* set the component to its initial state and re-fetch the images.
*/
fetch = (clear?: boolean) => {
if (!this.state.loadingMore) {
this.setState({loadingMore: true}, () => {
this._fetch(clear);
});
}
};
render() {
return (
<FlatList
keyExtractor={(_, idx) => String(idx)}
renderItem={this._renderItem}
ListFooterComponent={this._renderFooterSpinner}
onEndReached={this._onEndReached}
onEndReachedThreshold={0.2}
style={styles.container}
data={this.state.data || []}
extraData={this.props.bigImages + this.state.noMore}
/>
);
}
_renderFooterSpinner = () => {
if (!this.state.noMore) {
return <ActivityIndicator />;
}
return null;
};
_renderItem = (row: Row) => {
return (
<View style={styles.row}>
{row.item.map(image => (image ? this.props.renderImage(image) : null))}
</View>
);
};
_appendAssets(data: PhotoIdentifiersPage) {
const assets = data.edges;
const newState: $Shape<State> = {loadingMore: false};
if (!data.page_info.has_next_page) {
newState.noMore = true;
}
if (assets.length > 0) {
newState.lastCursor = data.page_info.end_cursor;
newState.seen = new Set(this.state.seen);
// Unique assets efficiently
// Checks new pages against seen objects
const uniqAssets = [];
for (let index = 0; index < assets.length; index++) {
const asset = assets[index];
let value = asset.node.image.uri;
if (newState.seen.has(value)) {
continue;
}
newState.seen.add(value);
uniqAssets.push(asset);
}
newState.assets = this.state.assets.concat(uniqAssets);
newState.data = groupByEveryN<PhotoIdentifier>(
newState.assets,
this.props.imagesPerRow,
);
}
this.setState(newState);
}
_onEndReached = () => {
if (!this.state.noMore) {
this.fetch();
}
};
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
flex: 1,
},
image: {
margin: 4,
},
container: {
flex: 1,
},
});
module.exports = CameraRollView;