Detox tests E2E (#283)

This commit is contained in:
Guilherme Gazzo 2018-05-23 10:39:18 -03:00 committed by GitHub
parent a9acbec05c
commit 182ab69d6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 2485 additions and 205 deletions

View File

@ -35,6 +35,52 @@ jobs:
command: | command: |
npx codecov npx codecov
e2e-test:
macos:
xcode: "9.0"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
steps:
- checkout
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- run:
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install applesimutils
- run:
name: Install NPM modules
command: |
rm -rf node_modules
npm install
npm install -g detox-cli
- run:
name: Build
command: |
detox build
- run:
name: Test
command: |
detox test
- store_artifacts:
path: /tmp/screenshots
android-build: android-build:
<<: *defaults <<: *defaults
docker: docker:
@ -215,10 +261,14 @@ workflows:
build-and-test: build-and-test:
jobs: jobs:
- lint-testunit - lint-testunit
- e2e-test:
requires:
- lint-testunit
- ios-build: - ios-build:
requires: requires:
- lint-testunit - lint-testunit
- e2e-test
- ios-testflight: - ios-testflight:
requires: requires:
- ios-build - ios-build
@ -235,3 +285,4 @@ workflows:
- android-build: - android-build:
requires: requires:
- lint-testunit - lint-testunit
- e2e-test

View File

@ -1,3 +1,4 @@
__tests__ __tests__
node_modules node_modules
coverage coverage
e2e

View File

@ -38,6 +38,19 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react
$ npm run android $ npm run android
``` ```
# Detox (end-to-end tests)
- Build your app
```bash
$ detox build
```
- Run tests
```bash
$ detox test
```
# Storybook # Storybook
- General requirements - General requirements
- Install storybook - Install storybook

View File

@ -12,5 +12,6 @@ if (__DEV__) {
.connect(); .connect();
// Running on android device // Running on android device
// $ adb reverse tcp:9090 tcp:9090 // $ adb reverse tcp:9090 tcp:9090
console.warn = Reactotron.log; // Reactotron.clear();
// console.warn = Reactotron.log;
} }

View File

@ -60,7 +60,7 @@ export default class Button extends React.PureComponent {
render() { render() {
const { const {
title, type, onPress, disabled title, type, onPress, disabled, ...otherProps
} = this.props; } = this.props;
return ( return (
<Touch <Touch
@ -68,6 +68,7 @@ export default class Button extends React.PureComponent {
accessibilityTraits='button' accessibilityTraits='button'
style={Platform.OS === 'ios' && styles.margin} style={Platform.OS === 'ios' && styles.margin}
disabled={disabled} disabled={disabled}
{...otherProps}
> >
<View <View
style={[ style={[

View File

@ -24,7 +24,11 @@ export default class CloseModalButton extends React.PureComponent {
render() { render() {
return ( return (
<TouchableOpacity onPress={() => this.props.navigation.dispatch(NavigationActions.back())} style={styles.button}> <TouchableOpacity
onPress={() => this.props.navigation.dispatch(NavigationActions.back())}
style={styles.button}
testID='close-modal-button'
>
<Icon <Icon
style={styles.icon} style={styles.icon}
name='close' name='close'

View File

@ -49,6 +49,7 @@ export default class EmojiCategory extends React.Component {
activeOpacity={0.7} activeOpacity={0.7}
key={emoji.isCustom ? emoji.content : emoji} key={emoji.isCustom ? emoji.content : emoji}
onPress={() => this.props.onEmojiSelected(emoji)} onPress={() => this.props.onEmojiSelected(emoji)}
testID={`reaction-picker-${ emoji.isCustom ? emoji.content : emoji }`}
> >
{renderEmoji(emoji, size)} {renderEmoji(emoji, size)}
</TouchableOpacity>); </TouchableOpacity>);

View File

@ -20,6 +20,7 @@ export default class TabBar extends React.PureComponent {
key={tab} key={tab}
onPress={() => this.props.goToPage(i)} onPress={() => this.props.goToPage(i)}
style={styles.tab} style={styles.tab}
testID={`reaction-picker-${ tab }`}
> >
<Text style={[styles.tabEmoji, this.props.tabEmojiStyle]}>{tab}</Text> <Text style={[styles.tabEmoji, this.props.tabEmojiStyle]}>{tab}</Text>
{this.props.activeTab === i ? <View style={styles.activeTabLine} /> : <View style={styles.tabLine} />} {this.props.activeTab === i ? <View style={styles.activeTabLine} /> : <View style={styles.tabLine} />}

View File

@ -337,6 +337,7 @@ export default class MessageActions extends React.Component {
<ActionSheet <ActionSheet
ref={o => this.ActionSheet = o} ref={o => this.ActionSheet = o}
title='Messages actions' title='Messages actions'
testID='message-actions'
options={this.options} options={this.options}
cancelButtonIndex={this.CANCEL_INDEX} cancelButtonIndex={this.CANCEL_INDEX}
destructiveButtonIndex={this.DELETE_INDEX} destructiveButtonIndex={this.DELETE_INDEX}

View File

@ -13,7 +13,7 @@ export default class EmojiKeyboard extends React.PureComponent {
render() { render() {
return ( return (
<Provider store={store}> <Provider store={store}>
<View style={styles.emojiKeyboardContainer}> <View style={styles.emojiKeyboardContainer} testID='messagebox-keyboard-emoji'>
<EmojiPicker onEmojiSelected={emoji => this.onEmojiSelected(emoji)} /> <EmojiPicker onEmojiSelected={emoji => this.onEmojiSelected(emoji)} />
</View> </View>
</Provider> </Provider>

View File

@ -100,7 +100,8 @@ export default class extends React.PureComponent {
render() { render() {
return ( return (
<SafeAreaView <SafeAreaView
key='messagebox' key='messagebox-recording'
testID='messagebox-recording'
style={styles.textBox} style={styles.textBox}
> >
<View style={[styles.textArea, { backgroundColor: '#F6F7F9' }]}> <View style={[styles.textArea, { backgroundColor: '#F6F7F9' }]}>

View File

@ -88,7 +88,6 @@ export default class MessageBox extends React.PureComponent {
const regexp = /(#|@|:)([a-z0-9._-]+)$/im; const regexp = /(#|@|:)([a-z0-9._-]+)$/im;
const result = lastNativeText.substr(0, cursor).match(regexp); const result = lastNativeText.substr(0, cursor).match(regexp);
if (!result) { if (!result) {
return this.stopTrackingMention(); return this.stopTrackingMention();
} }
@ -111,6 +110,7 @@ export default class MessageBox extends React.PureComponent {
accessibilityLabel='Cancel editing' accessibilityLabel='Cancel editing'
accessibilityTraits='button' accessibilityTraits='button'
onPress={() => this.editCancel()} onPress={() => this.editCancel()}
testID='messagebox-cancel-editing'
/>); />);
} }
return !this.state.showEmojiKeyboard ? (<Icon return !this.state.showEmojiKeyboard ? (<Icon
@ -119,12 +119,14 @@ export default class MessageBox extends React.PureComponent {
accessibilityLabel='Open emoji selector' accessibilityLabel='Open emoji selector'
accessibilityTraits='button' accessibilityTraits='button'
name='mood' name='mood'
testID='messagebox-open-emoji'
/>) : (<Icon />) : (<Icon
onPress={() => this.closeEmoji()} onPress={() => this.closeEmoji()}
style={styles.actionButtons} style={styles.actionButtons}
accessibilityLabel='Close emoji selector' accessibilityLabel='Close emoji selector'
accessibilityTraits='button' accessibilityTraits='button'
name='keyboard' name='keyboard'
testID='messagebox-close-emoji'
/>); />);
} }
get rightButtons() { get rightButtons() {
@ -138,6 +140,7 @@ export default class MessageBox extends React.PureComponent {
accessibilityLabel='Send message' accessibilityLabel='Send message'
accessibilityTraits='button' accessibilityTraits='button'
onPress={() => this.submit(this.state.text)} onPress={() => this.submit(this.state.text)}
testID='messagebox-send-message'
/>); />);
return icons; return icons;
} }
@ -148,6 +151,7 @@ export default class MessageBox extends React.PureComponent {
accessibilityLabel='Send audio message' accessibilityLabel='Send audio message'
accessibilityTraits='button' accessibilityTraits='button'
onPress={() => this.recordAudioMessage()} onPress={() => this.recordAudioMessage()}
testID='messagebox-send-audio'
/>); />);
icons.push(<MyIcon icons.push(<MyIcon
style={[styles.actionButtons, { color: '#2F343D', fontSize: 16 }]} style={[styles.actionButtons, { color: '#2F343D', fontSize: 16 }]}
@ -156,6 +160,7 @@ export default class MessageBox extends React.PureComponent {
accessibilityLabel='Message actions' accessibilityLabel='Message actions'
accessibilityTraits='button' accessibilityTraits='button'
onPress={() => this.addFile()} onPress={() => this.addFile()}
testID='messagebox-actions'
/>); />);
return icons; return icons;
} }
@ -438,6 +443,7 @@ export default class MessageBox extends React.PureComponent {
<TouchableOpacity <TouchableOpacity
style={styles.mentionItem} style={styles.mentionItem}
onPress={() => this._onPressMention(item)} onPress={() => this._onPressMention(item)}
testID={`mention-item-${ this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? item.name || item : item.username || item.name }`}
> >
{this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? {this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ?
[ [
@ -463,14 +469,15 @@ export default class MessageBox extends React.PureComponent {
return null; return null;
} }
return ( return (
<View key='messagebox-container' testID='messagebox-container'>
<FlatList <FlatList
key='messagebox-container'
style={styles.mentionList} style={styles.mentionList}
data={mentions} data={mentions}
renderItem={({ item }) => this.renderMentionItem(item)} renderItem={({ item }) => this.renderMentionItem(item)}
keyExtractor={item => item._id || item} keyExtractor={item => item._id || item}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
/> />
</View>
); );
}; };
@ -481,7 +488,11 @@ export default class MessageBox extends React.PureComponent {
return ( return (
[ [
this.renderMentions(), this.renderMentions(),
<View key='messagebox' style={[styles.textArea, this.props.editing && styles.editing]}> <View
key='messagebox'
style={[styles.textArea, this.props.editing && styles.editing]}
testID='messagebox'
>
{this.leftButtons} {this.leftButtons}
<TextInput <TextInput
ref={component => this.component = component} ref={component => this.component = component}
@ -496,6 +507,7 @@ export default class MessageBox extends React.PureComponent {
defaultValue='' defaultValue=''
multiline multiline
placeholderTextColor='#9EA2A8' placeholderTextColor='#9EA2A8'
testID='messagebox-input'
/> />
{this.rightButtons} {this.rightButtons}
</View> </View>

View File

@ -67,7 +67,7 @@ export default class Sidebar extends Component {
onPressItem = (item) => { onPressItem = (item) => {
this.props.selectServer(item.id); this.props.selectServer(item.id);
this.props.navigation.dispatch(DrawerActions.closeDrawer()); this.closeDrawer();
} }
getState = () => ({ getState = () => ({
@ -78,12 +78,17 @@ export default class Sidebar extends Component {
this.setState(this.getState()); this.setState(this.getState());
} }
closeDrawer = () => {
this.props.navigation.dispatch(DrawerActions.closeDrawer());
}
renderItem = ({ item, separators }) => ( renderItem = ({ item, separators }) => (
<TouchableHighlight <TouchableHighlight
onShowUnderlay={separators.highlight} onShowUnderlay={separators.highlight}
onHideUnderlay={separators.unhighlight} onHideUnderlay={separators.unhighlight}
onPress={() => { this.onPressItem(item); }} onPress={() => { this.onPressItem(item); }}
testID={`sidebar-${ item.id }`}
> >
<View style={[styles.serverItem, (item.id === this.props.server ? styles.selectedServer : null)]}> <View style={[styles.serverItem, (item.id === this.props.server ? styles.selectedServer : null)]}>
<Text> <Text>
@ -96,14 +101,18 @@ export default class Sidebar extends Component {
render() { render() {
return ( return (
<ScrollView style={styles.scrollView}> <ScrollView style={styles.scrollView}>
<View style={{ paddingBottom: 20 }}> <View style={{ paddingBottom: 20 }} testID='sidebar'>
<FlatList <FlatList
data={this.state.servers} data={this.state.servers}
renderItem={this.renderItem} renderItem={this.renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
/> />
<TouchableHighlight <TouchableHighlight
onPress={() => { this.props.logout(); }} onPress={() => {
this.closeDrawer();
this.props.logout();
}}
testID='sidebar-logout'
> >
<View style={styles.serverItem}> <View style={styles.serverItem}>
<Text> <Text>
@ -112,7 +121,11 @@ export default class Sidebar extends Component {
</View> </View>
</TouchableHighlight> </TouchableHighlight>
<TouchableHighlight <TouchableHighlight
onPress={() => { this.props.navigation.navigate({ key: 'AddServer', routeName: 'AddServer' }); }} onPress={() => {
this.closeDrawer();
this.props.navigation.navigate({ key: 'AddServer', routeName: 'AddServer' });
}}
testID='sidebar-add-server'
> >
<View style={styles.serverItem}> <View style={styles.serverItem}>
<Text> <Text>

View File

@ -75,22 +75,37 @@ export default class RCTextInput extends React.PureComponent {
showPassword: false showPassword: false
} }
icon = ({ name, onPress, style }) => <Icon name={name} style={[styles.icon, style]} size={20} onPress={onPress} /> icon = ({
name,
onPress,
style,
testID
}) => <Icon name={name} style={[styles.icon, style]} size={20} onPress={onPress} testID={testID} />
iconLeft = name => this.icon({ name, onPress: null, style: { left: 0 } }); iconLeft = name => this.icon({
name,
onPress: null,
style: { left: 0 },
testID: this.props.testID ? `${ this.props.testID }-icon-left` : null
});
iconPassword = name => this.icon({ name, onPress: () => this.tooglePassword(), style: { right: 0 } }); iconPassword = name => this.icon({
name,
onPress: () => this.tooglePassword(),
style: { right: 0 },
testID: this.props.testID ? `${ this.props.testID }-icon-right` : null
});
tooglePassword = () => this.setState({ showPassword: !this.state.showPassword }); tooglePassword = () => this.setState({ showPassword: !this.state.showPassword });
render() { render() {
const { const {
label, error, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, ...inputProps label, error, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, ...inputProps
} = this.props; } = this.props;
const { showPassword } = this.state; const { showPassword } = this.state;
return ( return (
<View style={[styles.inputContainer, containerStyle]}> <View style={[styles.inputContainer, containerStyle]}>
{ label && <Text style={[styles.label, error.error && styles.labelError]}>{label}</Text> } {label && <Text contentDescription={null} accessibilityLabel={null} style={[styles.label, error.error && styles.labelError]}>{label}</Text> }
<View style={styles.wrap}> <View style={styles.wrap}>
<TextInput <TextInput
style={[ style={[
@ -105,6 +120,10 @@ export default class RCTextInput extends React.PureComponent {
autoCapitalize='none' autoCapitalize='none'
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
secureTextEntry={secureTextEntry && !showPassword} secureTextEntry={secureTextEntry && !showPassword}
testID={testID}
accessibilityLabel={placeholder}
placeholder={placeholder}
contentDescription={placeholder}
{...inputProps} {...inputProps}
/> />
{iconLeft && this.iconLeft(iconLeft)} {iconLeft && this.iconLeft(iconLeft)}

View File

@ -270,6 +270,7 @@ export default class Message extends React.Component {
onPress={() => this.onReactionPress(reaction.emoji)} onPress={() => this.onReactionPress(reaction.emoji)}
onLongPress={() => this.onReactionLongPress()} onLongPress={() => this.onReactionLongPress()}
key={reaction.emoji} key={reaction.emoji}
testID={`message-reaction-${ reaction.emoji }`}
> >
<View style={[styles.reactionContainer, reactedContainerStyle]}> <View style={[styles.reactionContainer, reactedContainerStyle]}>
<Emoji <Emoji
@ -292,7 +293,8 @@ export default class Message extends React.Component {
{this.props.item.reactions.map(this.renderReaction)} {this.props.item.reactions.map(this.renderReaction)}
<TouchableOpacity <TouchableOpacity
onPress={() => this.props.toggleReactionPicker(this.parseMessage())} onPress={() => this.props.toggleReactionPicker(this.parseMessage())}
key='add-reaction' key='message-add-reaction'
testID='message-add-reaction'
style={styles.reactionContainer} style={styles.reactionContainer}
> >
<Icon name='insert-emoticon' color='#aaaaaa' size={15} /> <Icon name='insert-emoticon' color='#aaaaaa' size={15} />

View File

@ -40,6 +40,7 @@ async function canOpenRoomDDP(...args) {
export default async function canOpenRoom({ rid, path }) { export default async function canOpenRoom({ rid, path }) {
const { database: db } = database; const { database: db } = database;
const room = db.objects('subscriptions').filtered('rid == $0', rid); const room = db.objects('subscriptions').filtered('rid == $0', rid);
if (room.length) { if (room.length) {
return true; return true;

View File

@ -15,7 +15,7 @@ export const merge = (subscription, room) => {
subscription.joinCodeRequired = room.joinCodeRequired; subscription.joinCodeRequired = room.joinCodeRequired;
if (room.muted && room.muted.length) { if (room.muted && room.muted.length) {
subscription.muted = room.muted.filter(role => role).map(role => ({ value: role })); subscription.muted = room.muted.filter(user => user).map(user => ({ value: user }));
} }
} }
if (subscription.roles && subscription.roles.length) { if (subscription.roles && subscription.roles.length) {

View File

@ -21,9 +21,13 @@ export const getMessage = (rid, msg = {}) => {
username: reduxStore.getState().login.user.username username: reduxStore.getState().login.user.username
} }
}; };
try {
database.write(() => { database.write(() => {
database.create('messages', message, true); database.create('messages', message, true);
}); });
} catch (error) {
console.warn('getMessage', error);
}
return message; return message;
}; };

View File

@ -1,6 +1,8 @@
import Random from 'react-native-meteor/lib/Random';
import database from '../../realm'; import database from '../../realm';
import { merge } from '../helpers/mergeSubscriptionsRooms'; import { merge } from '../helpers/mergeSubscriptionsRooms';
import protectedFunction from '../helpers/protectedFunction'; import protectedFunction from '../helpers/protectedFunction';
import messagesStatus from '../../../constants/messagesStatus';
import log from '../../../utils/log'; import log from '../../../utils/log';
export default async function subscribeRooms(id) { export default async function subscribeRooms(id) {
@ -26,7 +28,9 @@ export default async function subscribeRooms(id) {
}, 5000); }, 5000);
}; };
if (this.ddp) { if (!this.ddp && this._login) {
loop();
} else {
this.ddp.on('logged', () => { this.ddp.on('logged', () => {
clearTimeout(timer); clearTimeout(timer);
timer = false; timer = false;
@ -48,7 +52,7 @@ export default async function subscribeRooms(id) {
const [, ev] = ddpMessage.fields.eventName.split('/'); const [, ev] = ddpMessage.fields.eventName.split('/');
if (/subscriptions/.test(ev)) { if (/subscriptions/.test(ev)) {
const tpm = merge(data); const tpm = merge(data);
return database.write(() => { database.write(() => {
database.create('subscriptions', tpm, true); database.create('subscriptions', tpm, true);
}); });
} }
@ -58,6 +62,25 @@ export default async function subscribeRooms(id) {
merge(sub, data); merge(sub, data);
}); });
} }
if (/message/.test(ev)) {
const [args] = ddpMessage.fields.args;
const _id = Random.id();
const message = {
_id,
rid: args.rid,
msg: args.msg,
ts: new Date(),
_updatedAt: new Date(),
status: messagesStatus.SENT,
u: {
_id,
username: 'rocket.cat'
}
};
requestAnimationFrame(() => database.write(() => {
database.create('messages', message, true);
}));
}
})); }));
} }

View File

@ -1,7 +1,6 @@
import { AsyncStorage, Platform } from 'react-native'; import { AsyncStorage, Platform } from 'react-native';
import { hashPassword } from 'react-native-meteor/lib/utils'; import { hashPassword } from 'react-native-meteor/lib/utils';
import foreach from 'lodash/forEach'; import foreach from 'lodash/forEach';
import Random from 'react-native-meteor/lib/Random';
import RNFetchBlob from 'react-native-fetch-blob'; import RNFetchBlob from 'react-native-fetch-blob';
import reduxStore from './createStore'; import reduxStore from './createStore';
@ -23,8 +22,6 @@ import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setRoles } from '../actions/roles'; import { setRoles } from '../actions/roles';
import Ddp from './ddp'; import Ddp from './ddp';
import normalizeMessage from './methods/helpers/normalizeMessage';
import subscribeRooms from './methods/subscriptions/rooms'; import subscribeRooms from './methods/subscriptions/rooms';
import subscribeRoom from './methods/subscriptions/room'; import subscribeRoom from './methods/subscriptions/room';
@ -167,9 +164,11 @@ const RocketChat = {
this.ddp.on('disconnected', protectedFunction(() => { this.ddp.on('disconnected', protectedFunction(() => {
reduxStore.dispatch(disconnect()); reduxStore.dispatch(disconnect());
console.warn(this.ddp);
})); }));
this.ddp.on('stream-room-messages', (ddpMessage) => { this.ddp.on('stream-room-messages', (ddpMessage) => {
// TODO: debounce
const message = _buildMessage(ddpMessage.fields.args[0]); const message = _buildMessage(ddpMessage.fields.args[0]);
requestAnimationFrame(() => reduxStore.dispatch(roomMessageReceived(message))); requestAnimationFrame(() => reduxStore.dispatch(roomMessageReceived(message)));
}); });
@ -182,65 +181,66 @@ const RocketChat = {
return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] })); return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
})); }));
this.ddp.on('stream-notify-user', protectedFunction((ddpMessage) => { // this.ddp.on('stream-notify-user', protectedFunction((ddpMessage) => {
const [type, data] = ddpMessage.fields.args; // console.warn('rc.stream-notify-user')
const [, ev] = ddpMessage.fields.eventName.split('/'); // const [type, data] = ddpMessage.fields.args;
if (/subscriptions/.test(ev)) { // const [, ev] = ddpMessage.fields.eventName.split('/');
if (data.roles) { // if (/subscriptions/.test(ev)) {
data.roles = data.roles.map(role => ({ value: role })); // if (data.roles) {
} // data.roles = data.roles.map(role => ({ value: role }));
if (data.blocker) { // }
data.blocked = true; // if (data.blocker) {
} else { // data.blocked = true;
data.blocked = false; // } else {
} // data.blocked = false;
if (data.mobilePushNotifications === 'nothing') { // }
data.notifications = true; // if (data.mobilePushNotifications === 'nothing') {
} else { // data.notifications = true;
data.notifications = false; // } else {
} // data.notifications = false;
database.write(() => { // }
database.create('subscriptions', data, true); // database.write(() => {
}); // database.create('subscriptions', data, true);
} // });
if (/rooms/.test(ev) && type === 'updated') { // }
const sub = database.objects('subscriptions').filtered('rid == $0', data._id)[0]; // if (/rooms/.test(ev) && type === 'updated') {
// const sub = database.objects('subscriptions').filtered('rid == $0', data._id)[0];
database.write(() => { // database.write(() => {
sub.roomUpdatedAt = data._updatedAt; // sub.roomUpdatedAt = data._updatedAt;
sub.lastMessage = normalizeMessage(data.lastMessage); // sub.lastMessage = normalizeMessage(data.lastMessage);
sub.ro = data.ro; // sub.ro = data.ro;
sub.description = data.description; // sub.description = data.description;
sub.topic = data.topic; // sub.topic = data.topic;
sub.announcement = data.announcement; // sub.announcement = data.announcement;
sub.reactWhenReadOnly = data.reactWhenReadOnly; // sub.reactWhenReadOnly = data.reactWhenReadOnly;
sub.archived = data.archived; // sub.archived = data.archived;
sub.joinCodeRequired = data.joinCodeRequired; // sub.joinCodeRequired = data.joinCodeRequired;
if (data.muted) { // if (data.muted) {
sub.muted = data.muted.map(m => ({ value: m })); // sub.muted = data.muted.map(m => ({ value: m }));
} // }
}); // });
} // }
if (/message/.test(ev)) { // if (/message/.test(ev)) {
const [args] = ddpMessage.fields.args; // const [args] = ddpMessage.fields.args;
const _id = Random.id(); // const _id = Random.id();
const message = { // const message = {
_id, // _id,
rid: args.rid, // rid: args.rid,
msg: args.msg, // msg: args.msg,
ts: new Date(), // ts: new Date(),
_updatedAt: new Date(), // _updatedAt: new Date(),
status: messagesStatus.SENT, // status: messagesStatus.SENT,
u: { // u: {
_id, // _id,
username: 'rocket.cat' // username: 'rocket.cat'
} // }
}; // };
requestAnimationFrame(() => database.write(() => { // requestAnimationFrame(() => database.write(() => {
database.create('messages', message, true); // database.create('messages', message, true);
})); // }));
} // }
})); // }));
this.ddp.on('rocketchat_starred_message', protectedFunction((ddpMessage) => { this.ddp.on('rocketchat_starred_message', protectedFunction((ddpMessage) => {
if (ddpMessage.msg === 'added') { if (ddpMessage.msg === 'added') {

View File

@ -171,7 +171,8 @@ export default class RoomItem extends React.Component {
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
user: PropTypes.object, user: PropTypes.object,
avatarSize: PropTypes.number, avatarSize: PropTypes.number,
statusStyle: ViewPropTypes.style statusStyle: ViewPropTypes.style,
testID: PropTypes.string
} }
static defaultProps = { static defaultProps = {
@ -238,7 +239,7 @@ export default class RoomItem extends React.Component {
render() { render() {
const { const {
favorite, unread, userMentions, name, _updatedAt, alert, type favorite, unread, userMentions, name, _updatedAt, alert, type, testID
} = this.props; } = this.props;
const date = this.formatDate(_updatedAt); const date = this.formatDate(_updatedAt);
@ -266,6 +267,7 @@ export default class RoomItem extends React.Component {
activeOpacity={0.5} activeOpacity={0.5}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
accessibilityTraits='selected' accessibilityTraits='selected'
testID={testID}
> >
<View style={[styles.container, favorite && styles.favorite]}> <View style={[styles.container, favorite && styles.favorite]}>
{this.icon} {this.icon}

View File

@ -164,7 +164,7 @@ const watchLoginOpen = function* watchLoginOpen() {
} }
const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration'); const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration');
yield take(types.LOGIN.CLOSE); yield take(types.LOGIN.CLOSE);
sub.unsubscribe(); yield sub.unsubscribe().catch(err => console.warn(err));
} catch (e) { } catch (e) {
log('watchLoginOpen', e); log('watchLoginOpen', e);
} }

View File

@ -140,12 +140,16 @@ const updateLastOpen = function* updateLastOpen() {
const goRoomsListAndDelete = function* goRoomsListAndDelete(rid) { const goRoomsListAndDelete = function* goRoomsListAndDelete(rid) {
NavigationService.goRoomsList(); NavigationService.goRoomsList();
yield delay(1000); yield delay(1000);
try {
database.write(() => { database.write(() => {
const messages = database.objects('messages').filtered('rid = $0', rid); const messages = database.objects('messages').filtered('rid = $0', rid);
database.delete(messages); database.delete(messages);
const subscription = database.objects('subscriptions').filtered('rid = $0', rid); const subscription = database.objects('subscriptions').filtered('rid = $0', rid);
database.delete(subscription); database.delete(subscription);
}); });
} catch (error) {
console.warn('goRoomsListAndDelete', error);
}
}; };
const handleLeaveRoom = function* handleLeaveRoom({ rid }) { const handleLeaveRoom = function* handleLeaveRoom({ rid }) {

View File

@ -62,7 +62,7 @@ export default class CreateChannelView extends LoggedView {
} }
return ( return (
<Text style={[styles.label_white, styles.label_error]}> <Text style={[styles.label_white, styles.label_error]} testID='create-channel-error'>
{this.props.createChannel.error.reason} {this.props.createChannel.error.reason}
</Text> </Text>
); );
@ -75,6 +75,7 @@ export default class CreateChannelView extends LoggedView {
style={[{ flexGrow: 0, flexShrink: 1 }]} style={[{ flexGrow: 0, flexShrink: 1 }]}
value={this.state.type} value={this.state.type}
onValueChange={type => this.setState({ type })} onValueChange={type => this.setState({ type })}
testID='create-channel-type'
/> />
<Text style={[styles.label_white, styles.switchLabel]}> <Text style={[styles.label_white, styles.switchLabel]}>
{this.state.type ? 'Public' : 'Private'} {this.state.type ? 'Public' : 'Private'}
@ -90,7 +91,7 @@ export default class CreateChannelView extends LoggedView {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView> <SafeAreaView testID='create-channel-view'>
<RCTextInput <RCTextInput
label='Channel Name' label='Channel Name'
value={this.state.channelName} value={this.state.channelName}
@ -98,6 +99,7 @@ export default class CreateChannelView extends LoggedView {
placeholder='Type the channel name here' placeholder='Type the channel name here'
returnKeyType='done' returnKeyType='done'
autoFocus autoFocus
testID='create-channel-name'
/> />
{this.renderChannelNameError()} {this.renderChannelNameError()}
{this.renderTypeSwitch()} {this.renderTypeSwitch()}
@ -126,6 +128,7 @@ export default class CreateChannelView extends LoggedView {
? styles.disabledButton ? styles.disabledButton
: styles.enabledButton : styles.enabledButton
]} ]}
testID='create-channel-submit'
> >
<Text style={styles.button_white}>CREATE</Text> <Text style={styles.button_white}>CREATE</Text>
</TouchableOpacity> </TouchableOpacity>

View File

@ -80,7 +80,7 @@ export default class ForgotPasswordView extends LoggedView {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView> <SafeAreaView testID='forgot-password-view'>
<View style={styles.loginView}> <View style={styles.loginView}>
<View style={styles.formContainer}> <View style={styles.formContainer}>
<TextInput <TextInput
@ -91,6 +91,7 @@ export default class ForgotPasswordView extends LoggedView {
returnKeyType='next' returnKeyType='next'
onChangeText={email => this.validate(email)} onChangeText={email => this.validate(email)}
onSubmitEditing={() => this.resetPassword()} onSubmitEditing={() => this.resetPassword()}
testID='forgot-password-view-email'
/> />
<View style={styles.alignItemsFlexStart}> <View style={styles.alignItemsFlexStart}>
@ -98,6 +99,7 @@ export default class ForgotPasswordView extends LoggedView {
title='Reset password' title='Reset password'
type='primary' type='primary'
onPress={this.resetPassword} onPress={this.resetPassword}
testID='forgot-password-view-submit'
/> />
</View> </View>

View File

@ -209,7 +209,7 @@ class ListServerView extends LoggedView {
render() { render() {
return ( return (
<SafeAreaView style={styles.view}> <SafeAreaView style={styles.view} testID='list-server-view'>
<SectionList <SectionList
style={styles.list} style={styles.list}
sections={this.state.sections} sections={this.state.sections}

View File

@ -277,7 +277,7 @@ export default class LoginSignupView extends LoggedView {
style={[sharedStyles.container, sharedStyles.containerScrollView]} style={[sharedStyles.container, sharedStyles.containerScrollView]}
{...scrollPersistTaps} {...scrollPersistTaps}
> >
<SafeAreaView> <SafeAreaView testID='welcome-view'>
<View style={styles.container}> <View style={styles.container}>
<Image <Image
source={require('../static/images/logo.png')} source={require('../static/images/logo.png')}
@ -294,11 +294,13 @@ export default class LoginSignupView extends LoggedView {
title='I have an account' title='I have an account'
type='primary' type='primary'
onPress={() => this.props.navigation.navigate({ key: 'Login', routeName: 'Login' })} onPress={() => this.props.navigation.navigate({ key: 'Login', routeName: 'Login' })}
testID='welcome-view-login'
/> />
<Button <Button
title='Create account' title='Create account'
type='secondary' type='secondary'
onPress={() => this.props.navigation.navigate({ key: 'Register', routeName: 'Register' })} onPress={() => this.props.navigation.navigate({ key: 'Register', routeName: 'Register' })}
testID='welcome-view-register'
/> />
{this.renderServices()} {this.renderServices()}
</View> </View>

View File

@ -83,7 +83,7 @@ export default class LoginView extends LoggedView {
key='login-view' key='login-view'
> >
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView> <SafeAreaView testID='login-view'>
<CloseModalButton navigation={this.props.navigation} /> <CloseModalButton navigation={this.props.navigation} />
<Text style={[styles.loginText, styles.loginTitle]}>Login</Text> <Text style={[styles.loginText, styles.loginTitle]}>Login</Text>
<TextInput <TextInput
@ -94,6 +94,7 @@ export default class LoginView extends LoggedView {
iconLeft='at' iconLeft='at'
onChangeText={username => this.setState({ username })} onChangeText={username => this.setState({ username })}
onSubmitEditing={() => { this.password.focus(); }} onSubmitEditing={() => { this.password.focus(); }}
testID='login-view-email'
/> />
<TextInput <TextInput
@ -105,6 +106,7 @@ export default class LoginView extends LoggedView {
secureTextEntry secureTextEntry
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
onChangeText={password => this.setState({ password })} onChangeText={password => this.setState({ password })}
testID='login-view-password'
/> />
{this.renderTOTP()} {this.renderTOTP()}
@ -114,17 +116,20 @@ export default class LoginView extends LoggedView {
title='Login' title='Login'
type='primary' type='primary'
onPress={this.submit} onPress={this.submit}
testID='login-view-submit'
/> />
<Text style={[styles.loginText, { marginTop: 10 }]}>New in Rocket.Chat? &nbsp;
<Text <Text
style={{ color: COLOR_BUTTON_PRIMARY }} style={[styles.loginText, { marginTop: 10 }]}
testID='login-view-register'
onPress={() => this.props.navigation.navigate('Register')} onPress={() => this.props.navigation.navigate('Register')}
>Sign Up >New in Rocket.Chat? &nbsp;
<Text style={{ color: COLOR_BUTTON_PRIMARY }}>Sign Up
</Text> </Text>
</Text> </Text>
<Text <Text
style={[styles.loginText, { marginTop: 20, fontSize: 13 }]} style={[styles.loginText, { marginTop: 20, fontSize: 13 }]}
onPress={() => this.props.navigation.navigate('ForgotPassword')} onPress={() => this.props.navigation.navigate('ForgotPassword')}
testID='login-view-forgot-password'
>Forgot password >Forgot password
</Text> </Text>
</View> </View>

View File

@ -73,7 +73,7 @@ export default class MentionedMessagesView extends LoggedView {
} }
renderEmpty = () => ( renderEmpty = () => (
<View style={styles.listEmptyContainer}> <View style={styles.listEmptyContainer} testID='mentioned-messages-view'>
<Text>No mentioned messages</Text> <Text>No mentioned messages</Text>
</View> </View>
) )
@ -99,8 +99,10 @@ export default class MentionedMessagesView extends LoggedView {
} }
return ( return (
[
<FlatList <FlatList
key='mentioned-messages-view-list' key='mentioned-messages-view-list'
testID='mentioned-messages-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -109,6 +111,7 @@ export default class MentionedMessagesView extends LoggedView {
ListHeaderComponent={loading && <RCActivityIndicator />} ListHeaderComponent={loading && <RCActivityIndicator />}
ListFooterComponent={loadingMore && <RCActivityIndicator />} ListFooterComponent={loadingMore && <RCActivityIndicator />}
/> />
]
); );
} }
} }

View File

@ -106,7 +106,7 @@ export default class NewServerView extends LoggedView {
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView> <SafeAreaView testID='new-server-view'>
<Text style={[styles.loginText, styles.loginTitle]}>Sign in your server</Text> <Text style={[styles.loginText, styles.loginTitle]}>Sign in your server</Text>
<TextInput <TextInput
inputRef={e => this.input = e} inputRef={e => this.input = e}
@ -115,6 +115,7 @@ export default class NewServerView extends LoggedView {
placeholder={this.state.defaultServer} placeholder={this.state.defaultServer}
returnKeyType='done' returnKeyType='done'
onChangeText={this.onChangeText} onChangeText={this.onChangeText}
testID='new-server-view-input'
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
/> />
{this.renderValidation()} {this.renderValidation()}
@ -124,6 +125,7 @@ export default class NewServerView extends LoggedView {
type='primary' type='primary'
onPress={this.submit} onPress={this.submit}
disabled={!validInstance} disabled={!validInstance}
testID='new-server-view-button'
/> />
</View> </View>
<Loading visible={this.props.addingServer} /> <Loading visible={this.props.addingServer} />

View File

@ -97,7 +97,7 @@ export default class PinnedMessagesView extends LoggedView {
} }
renderEmpty = () => ( renderEmpty = () => (
<View style={styles.listEmptyContainer}> <View style={styles.listEmptyContainer} testID='pinned-messages-view'>
<Text>No pinned messages</Text> <Text>No pinned messages</Text>
</View> </View>
) )
@ -126,6 +126,7 @@ export default class PinnedMessagesView extends LoggedView {
[ [
<FlatList <FlatList
key='pinned-messages-view-list' key='pinned-messages-view-list'
testID='pinned-messages-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}

View File

@ -108,6 +108,7 @@ export default class RegisterView extends LoggedView {
iconLeft='account' iconLeft='account'
onChangeText={name => this.setState({ name })} onChangeText={name => this.setState({ name })}
onSubmitEditing={() => { this.email.focus(); }} onSubmitEditing={() => { this.email.focus(); }}
testID='register-view-name'
/> />
<TextInput <TextInput
inputRef={(e) => { this.email = e; }} inputRef={(e) => { this.email = e; }}
@ -119,6 +120,7 @@ export default class RegisterView extends LoggedView {
onChangeText={email => this.setState({ email })} onChangeText={email => this.setState({ email })}
onSubmitEditing={() => { this.password.focus(); }} onSubmitEditing={() => { this.password.focus(); }}
error={this.invalidEmail()} error={this.invalidEmail()}
testID='register-view-email'
/> />
<TextInput <TextInput
inputRef={(e) => { this.password = e; }} inputRef={(e) => { this.password = e; }}
@ -129,6 +131,7 @@ export default class RegisterView extends LoggedView {
secureTextEntry secureTextEntry
onChangeText={password => this.setState({ password })} onChangeText={password => this.setState({ password })}
onSubmitEditing={() => { this.confirmPassword.focus(); }} onSubmitEditing={() => { this.confirmPassword.focus(); }}
testID='register-view-password'
/> />
<TextInput <TextInput
inputRef={(e) => { this.confirmPassword = e; }} inputRef={(e) => { this.confirmPassword = e; }}
@ -144,6 +147,7 @@ export default class RegisterView extends LoggedView {
secureTextEntry secureTextEntry
onChangeText={confirmPassword => this.setState({ confirmPassword })} onChangeText={confirmPassword => this.setState({ confirmPassword })}
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
testID='register-view-repeat-password'
/> />
<View style={styles.alignItemsFlexStart}> <View style={styles.alignItemsFlexStart}>
@ -157,10 +161,9 @@ export default class RegisterView extends LoggedView {
title='Register' title='Register'
type='primary' type='primary'
onPress={this.submit} onPress={this.submit}
testID='register-view-submit'
/> />
</View> </View>
{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
</View> </View>
); );
} }
@ -179,6 +182,7 @@ export default class RegisterView extends LoggedView {
iconLeft='at' iconLeft='at'
onChangeText={username => this.setState({ username })} onChangeText={username => this.setState({ username })}
onSubmitEditing={() => { this.usernameSubmit(); }} onSubmitEditing={() => { this.usernameSubmit(); }}
testID='register-view-username'
/> />
<View style={styles.alignItemsFlexStart}> <View style={styles.alignItemsFlexStart}>
@ -186,10 +190,9 @@ export default class RegisterView extends LoggedView {
title='Register' title='Register'
type='primary' type='primary'
onPress={this.usernameSubmit} onPress={this.usernameSubmit}
testID='register-view-submit-username'
/> />
</View> </View>
{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
</View> </View>
); );
} }
@ -198,11 +201,16 @@ export default class RegisterView extends LoggedView {
return ( return (
<KeyboardView contentContainerStyle={styles.container}> <KeyboardView contentContainerStyle={styles.container}>
<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<SafeAreaView> <SafeAreaView testID='register-view'>
<CloseModalButton navigation={this.props.navigation} /> <CloseModalButton navigation={this.props.navigation} />
<Text style={[styles.loginText, styles.loginTitle]}>Sign Up</Text> <Text style={[styles.loginText, styles.loginTitle]}>Sign Up</Text>
{this._renderRegister()} {this._renderRegister()}
{this._renderUsername()} {this._renderUsername()}
{this.props.login.failure &&
<Text style={styles.error} testID='register-view-error'>
{this.props.login.error.reason}
</Text>
}
<Loading visible={this.props.login.isFetching} /> <Loading visible={this.props.login.isFetching} />
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>

View File

@ -134,13 +134,24 @@ export default class RoomActionsView extends LoggedView {
icon: 'ios-star', icon: 'ios-star',
name: 'USER', name: 'USER',
route: 'RoomInfo', route: 'RoomInfo',
params: { rid } params: { rid },
testID: 'room-actions-info'
}], }],
renderItem: this.renderRoomInfo renderItem: this.renderRoomInfo
}, { }, {
data: [ data: [
{ icon: 'ios-call-outline', name: 'Voice call', disabled: true }, {
{ icon: 'ios-videocam-outline', name: 'Video call', disabled: true } icon: 'ios-call-outline',
name: 'Voice call',
disabled: true,
testID: 'room-actions-voice'
},
{
icon: 'ios-videocam-outline',
name: 'Video call',
disabled: true,
testID: 'room-actions-video'
}
], ],
renderItem: this.renderItem renderItem: this.renderItem
}, { }, {
@ -149,43 +160,55 @@ export default class RoomActionsView extends LoggedView {
icon: 'ios-attach', icon: 'ios-attach',
name: 'Files', name: 'Files',
route: 'RoomFiles', route: 'RoomFiles',
params: { rid } params: { rid },
testID: 'room-actions-files'
}, },
{ {
icon: 'ios-at-outline', icon: 'ios-at-outline',
name: 'Mentions', name: 'Mentions',
route: 'MentionedMessages', route: 'MentionedMessages',
params: { rid } params: { rid },
testID: 'room-actions-mentioned'
}, },
{ {
icon: 'ios-star-outline', icon: 'ios-star-outline',
name: 'Starred', name: 'Starred',
route: 'StarredMessages', route: 'StarredMessages',
params: { rid } params: { rid },
testID: 'room-actions-starred'
}, },
{ {
icon: 'ios-search', icon: 'ios-search',
name: 'Search', name: 'Search',
route: 'SearchMessages', route: 'SearchMessages',
params: { rid } params: { rid },
testID: 'room-actions-search'
},
{
icon: 'ios-share-outline',
name: 'Share',
disabled: true,
testID: 'room-actions-share'
}, },
{ icon: 'ios-share-outline', name: 'Share', disabled: true },
{ {
icon: 'ios-pin', icon: 'ios-pin',
name: 'Pinned', name: 'Pinned',
route: 'PinnedMessages', route: 'PinnedMessages',
params: { rid } params: { rid },
testID: 'room-actions-pinned'
}, },
{ {
icon: 'ios-code', icon: 'ios-code',
name: 'Snippets', name: 'Snippets',
route: 'SnippetedMessages', route: 'SnippetedMessages',
params: { rid } params: { rid },
testID: 'room-actions-snippeted'
}, },
{ {
icon: `ios-notifications${ notifications ? '' : '-off' }-outline`, icon: `ios-notifications${ notifications ? '' : '-off' }-outline`,
name: `${ notifications ? 'Enable' : 'Disable' } notifications`, name: `${ notifications ? 'Enable' : 'Disable' } notifications`,
event: () => this.toggleNotifications() event: () => this.toggleNotifications(),
testID: 'room-actions-notifications'
} }
], ],
renderItem: this.renderItem renderItem: this.renderItem
@ -198,7 +221,8 @@ export default class RoomActionsView extends LoggedView {
icon: 'block', icon: 'block',
name: `${ blocked ? 'Unblock' : 'Block' } user`, name: `${ blocked ? 'Unblock' : 'Block' } user`,
type: 'danger', type: 'danger',
event: () => this.toggleBlockUser() event: () => this.toggleBlockUser(),
testID: 'room-actions-block-user'
} }
], ],
renderItem: this.renderItem renderItem: this.renderItem
@ -209,7 +233,8 @@ export default class RoomActionsView extends LoggedView {
name: 'Members', name: 'Members',
description: (onlineMembers.length === 1 ? `${ onlineMembers.length } member` : `${ onlineMembers.length } members`), description: (onlineMembers.length === 1 ? `${ onlineMembers.length } member` : `${ onlineMembers.length } members`),
route: 'RoomMembers', route: 'RoomMembers',
params: { rid, members: onlineMembers } params: { rid, members: onlineMembers },
testID: 'room-actions-members'
}]; }];
if (this.canAddUser) { if (this.canAddUser) {
@ -229,7 +254,8 @@ export default class RoomActionsView extends LoggedView {
this.props.setLoadingInvite(false); this.props.setLoadingInvite(false);
} }
} }
} },
testID: 'room-actions-add-user'
}); });
} }
sections[2].data = [...actions, ...sections[2].data]; sections[2].data = [...actions, ...sections[2].data];
@ -239,7 +265,8 @@ export default class RoomActionsView extends LoggedView {
icon: 'block', icon: 'block',
name: 'Leave channel', name: 'Leave channel',
type: 'danger', type: 'danger',
event: () => this.leaveChannel() event: () => this.leaveChannel(),
testID: 'room-actions-leave-channel'
} }
], ],
renderItem: this.renderItem renderItem: this.renderItem
@ -248,7 +275,7 @@ export default class RoomActionsView extends LoggedView {
return sections; return sections;
} }
toggleBlockUser = () => { toggleBlockUser = async() => {
const { rid, blocked } = this.state.room; const { rid, blocked } = this.state.room;
const { member } = this.state; const { member } = this.state;
try { try {
@ -316,6 +343,7 @@ export default class RoomActionsView extends LoggedView {
activeOpacity={0.5} activeOpacity={0.5}
accessibilityLabel={item.name} accessibilityLabel={item.name}
accessibilityTraits='button' accessibilityTraits='button'
testID={item.testID}
> >
<View style={[styles.sectionItem, item.disabled && styles.sectionItemDisabled]}> <View style={[styles.sectionItem, item.disabled && styles.sectionItemDisabled]}>
{subview} {subview}
@ -348,6 +376,7 @@ export default class RoomActionsView extends LoggedView {
render() { render() {
return ( return (
<View testID='room-actions-view'>
<SectionList <SectionList
style={styles.container} style={styles.container}
stickySectionHeadersEnabled={false} stickySectionHeadersEnabled={false}
@ -355,7 +384,9 @@ export default class RoomActionsView extends LoggedView {
SectionSeparatorComponent={this.renderSectionSeparator} SectionSeparatorComponent={this.renderSectionSeparator}
ItemSeparatorComponent={renderSeparator} ItemSeparatorComponent={renderSeparator}
keyExtractor={item => item.name} keyExtractor={item => item.name}
testID='room-actions-list'
/> />
</View>
); );
} }
} }

View File

@ -73,7 +73,7 @@ export default class RoomFilesView extends LoggedView {
} }
renderEmpty = () => ( renderEmpty = () => (
<View style={styles.listEmptyContainer}> <View style={styles.listEmptyContainer} testID='room-files-view'>
<Text>No files</Text> <Text>No files</Text>
</View> </View>
) )
@ -97,8 +97,10 @@ export default class RoomFilesView extends LoggedView {
const { loading, loadingMore } = this.state; const { loading, loadingMore } = this.state;
return ( return (
[
<FlatList <FlatList
key='room-files-view-list' key='room-files-view-list'
testID='room-files-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -107,6 +109,7 @@ export default class RoomFilesView extends LoggedView {
ListHeaderComponent={loading && <RCActivityIndicator />} ListHeaderComponent={loading && <RCActivityIndicator />}
ListFooterComponent={loadingMore && <RCActivityIndicator />} ListFooterComponent={loadingMore && <RCActivityIndicator />}
/> />
]
); );
} }
} }

View File

@ -13,12 +13,13 @@ export default class SwitchContainer extends React.PureComponent {
leftLabelSecondary: PropTypes.string, leftLabelSecondary: PropTypes.string,
rightLabelPrimary: PropTypes.string, rightLabelPrimary: PropTypes.string,
rightLabelSecondary: PropTypes.string, rightLabelSecondary: PropTypes.string,
onValueChange: PropTypes.func onValueChange: PropTypes.func,
testID: PropTypes.string
} }
render() { render() {
const { const {
value, disabled, onValueChange, leftLabelPrimary, leftLabelSecondary, rightLabelPrimary, rightLabelSecondary value, disabled, onValueChange, leftLabelPrimary, leftLabelSecondary, rightLabelPrimary, rightLabelSecondary, testID
} = this.props; } = this.props;
return ( return (
[ [
@ -32,6 +33,7 @@ export default class SwitchContainer extends React.PureComponent {
onValueChange={onValueChange} onValueChange={onValueChange}
value={value} value={value}
disabled={disabled} disabled={disabled}
testID={testID}
/> />
<View style={styles.switchLabelContainer}> <View style={styles.switchLabelContainer}>
<Text style={styles.switchLabelPrimary}>{rightLabelPrimary}</Text> <Text style={styles.switchLabelPrimary}>{rightLabelPrimary}</Text>

View File

@ -74,9 +74,9 @@ export default class RoomInfoEditView extends LoggedView {
this.rooms.removeAllListeners(); this.rooms.removeAllListeners();
} }
updateRoom = () => { updateRoom = async() => {
const [room] = this.rooms; const [room] = this.rooms;
this.setState({ room }); await this.setState({ room });
} }
init = () => { init = () => {
@ -259,8 +259,12 @@ export default class RoomInfoEditView extends LoggedView {
contentContainerStyle={sharedStyles.container} contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
> >
<ScrollView {...scrollPersistTaps} contentContainerStyle={sharedStyles.containerScrollView}> <ScrollView
<SafeAreaView> contentContainerStyle={sharedStyles.containerScrollView}
testID='room-info-edit-view-list'
{...scrollPersistTaps}
>
<SafeAreaView testID='room-info-edit-view'>
<View style={sharedStyles.formContainer}> <View style={sharedStyles.formContainer}>
<RCTextInput <RCTextInput
inputRef={(e) => { this.name = e; }} inputRef={(e) => { this.name = e; }}
@ -269,6 +273,7 @@ export default class RoomInfoEditView extends LoggedView {
onChangeText={value => this.setState({ name: value })} onChangeText={value => this.setState({ name: value })}
onSubmitEditing={() => { this.description.focus(); }} onSubmitEditing={() => { this.description.focus(); }}
error={nameError} error={nameError}
testID='room-info-edit-view-name'
/> />
<RCTextInput <RCTextInput
inputRef={(e) => { this.description = e; }} inputRef={(e) => { this.description = e; }}
@ -276,6 +281,7 @@ export default class RoomInfoEditView extends LoggedView {
value={description} value={description}
onChangeText={value => this.setState({ description: value })} onChangeText={value => this.setState({ description: value })}
onSubmitEditing={() => { this.topic.focus(); }} onSubmitEditing={() => { this.topic.focus(); }}
testID='room-info-edit-view-description'
/> />
<RCTextInput <RCTextInput
inputRef={(e) => { this.topic = e; }} inputRef={(e) => { this.topic = e; }}
@ -283,6 +289,7 @@ export default class RoomInfoEditView extends LoggedView {
value={topic} value={topic}
onChangeText={value => this.setState({ topic: value })} onChangeText={value => this.setState({ topic: value })}
onSubmitEditing={() => { this.announcement.focus(); }} onSubmitEditing={() => { this.announcement.focus(); }}
testID='room-info-edit-view-topic'
/> />
<RCTextInput <RCTextInput
inputRef={(e) => { this.announcement = e; }} inputRef={(e) => { this.announcement = e; }}
@ -290,6 +297,7 @@ export default class RoomInfoEditView extends LoggedView {
value={announcement} value={announcement}
onChangeText={value => this.setState({ announcement: value })} onChangeText={value => this.setState({ announcement: value })}
onSubmitEditing={() => { this.joinCode.focus(); }} onSubmitEditing={() => { this.joinCode.focus(); }}
testID='room-info-edit-view-announcement'
/> />
<RCTextInput <RCTextInput
inputRef={(e) => { this.joinCode = e; }} inputRef={(e) => { this.joinCode = e; }}
@ -298,6 +306,7 @@ export default class RoomInfoEditView extends LoggedView {
onChangeText={value => this.setState({ joinCode: value })} onChangeText={value => this.setState({ joinCode: value })}
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
secureTextEntry secureTextEntry
testID='room-info-edit-view-password'
/> />
<SwitchContainer <SwitchContainer
value={t} value={t}
@ -306,6 +315,7 @@ export default class RoomInfoEditView extends LoggedView {
rightLabelPrimary='Private' rightLabelPrimary='Private'
rightLabelSecondary='Just invited people can access this channel' rightLabelSecondary='Just invited people can access this channel'
onValueChange={value => this.setState({ t: value })} onValueChange={value => this.setState({ t: value })}
testID='room-info-edit-view-t'
/> />
<SwitchContainer <SwitchContainer
value={ro} value={ro}
@ -315,6 +325,7 @@ export default class RoomInfoEditView extends LoggedView {
rightLabelSecondary='Only authorized users can write new messages' rightLabelSecondary='Only authorized users can write new messages'
onValueChange={value => this.setState({ ro: value })} onValueChange={value => this.setState({ ro: value })}
disabled={!this.permissions[PERMISSION_SET_READONLY]} disabled={!this.permissions[PERMISSION_SET_READONLY]}
testID='room-info-edit-view-ro'
/> />
{ro && {ro &&
<SwitchContainer <SwitchContainer
@ -325,12 +336,14 @@ export default class RoomInfoEditView extends LoggedView {
rightLabelSecondary='Reactions are enabled' rightLabelSecondary='Reactions are enabled'
onValueChange={value => this.setState({ reactWhenReadOnly: value })} onValueChange={value => this.setState({ reactWhenReadOnly: value })}
disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]} disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
testID='room-info-edit-view-react-when-ro'
/> />
} }
<TouchableOpacity <TouchableOpacity
style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]} style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]}
onPress={this.submit} onPress={this.submit}
disabled={!this.formIsChanged()} disabled={!this.formIsChanged()}
testID='room-info-edit-view-submit'
> >
<Text style={sharedStyles.button} accessibilityTraits='button'>SAVE</Text> <Text style={sharedStyles.button} accessibilityTraits='button'>SAVE</Text>
</TouchableOpacity> </TouchableOpacity>
@ -338,6 +351,7 @@ export default class RoomInfoEditView extends LoggedView {
<TouchableOpacity <TouchableOpacity
style={[sharedStyles.buttonContainer_inverted, styles.buttonInverted, { flex: 1 }]} style={[sharedStyles.buttonContainer_inverted, styles.buttonInverted, { flex: 1 }]}
onPress={this.reset} onPress={this.reset}
testID='room-info-edit-view-reset'
> >
<Text style={sharedStyles.button_inverted} accessibilityTraits='button'>RESET</Text> <Text style={sharedStyles.button_inverted} accessibilityTraits='button'>RESET</Text>
</TouchableOpacity> </TouchableOpacity>
@ -350,6 +364,7 @@ export default class RoomInfoEditView extends LoggedView {
]} ]}
onPress={this.toggleArchive} onPress={this.toggleArchive}
disabled={!this.hasArchivePermission()} disabled={!this.hasArchivePermission()}
testID='room-info-edit-view-archive'
> >
<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'> <Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>
{ room.archived ? 'UNARCHIVE' : 'ARCHIVE' } { room.archived ? 'UNARCHIVE' : 'ARCHIVE' }
@ -366,6 +381,7 @@ export default class RoomInfoEditView extends LoggedView {
]} ]}
onPress={this.delete} onPress={this.delete}
disabled={!this.hasDeletePermission()} disabled={!this.hasDeletePermission()}
testID='room-info-edit-view-delete'
> >
<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>DELETE</Text> <Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>DELETE</Text>
</TouchableOpacity> </TouchableOpacity>

View File

@ -20,7 +20,10 @@ import RoomTypeIcon from '../../containers/RoomTypeIcon';
const PERMISSION_EDIT_ROOM = 'edit-room'; const PERMISSION_EDIT_ROOM = 'edit-room';
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase()); const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
const getRoomTitle = room => (room.t === 'd' ? <Text>{room.fname}</Text> : <Text><RoomTypeIcon type={room.t} />&nbsp;{room.name}</Text>); const getRoomTitle = room => (room.t === 'd' ?
<Text testID='room-info-view-name'>{room.fname}</Text> :
[<RoomTypeIcon type={room.t} />, <Text testID='room-info-view-name'>{room.name}</Text>]
);
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: state.login.user, user: state.login.user,
@ -52,6 +55,7 @@ export default class RoomInfoView extends LoggedView {
activeOpacity={0.5} activeOpacity={0.5}
accessibilityLabel='edit' accessibilityLabel='edit'
accessibilityTraits='button' accessibilityTraits='button'
testID='room-info-view-edit-button'
> >
<View style={styles.headerButton}> <View style={styles.headerButton}>
<MaterialIcon name='edit' size={20} /> <MaterialIcon name='edit' size={20} />
@ -129,7 +133,11 @@ export default class RoomInfoView extends LoggedView {
renderItem = (key, room) => ( renderItem = (key, room) => (
<View style={styles.item}> <View style={styles.item}>
<Text style={styles.itemLabel}>{camelize(key)}</Text> <Text style={styles.itemLabel}>{camelize(key)}</Text>
<Text style={[styles.itemContent, !room[key] && styles.itemContent__empty]}>{ room[key] ? room[key] : `No ${ key } provided.` }</Text> <Text
style={[styles.itemContent, !room[key] && styles.itemContent__empty]}
testID={`room-info-view-${ key }`}
>{ room[key] ? room[key] : `No ${ key } provided.` }
</Text>
</View> </View>
); );
@ -183,9 +191,9 @@ export default class RoomInfoView extends LoggedView {
} }
return ( return (
<ScrollView style={styles.container}> <ScrollView style={styles.container}>
<View style={styles.avatarContainer}> <View style={styles.avatarContainer} testID='room-info-view'>
{this.renderAvatar(room, roomUser)} {this.renderAvatar(room, roomUser)}
<Text style={styles.roomTitle}>{ getRoomTitle(room) }</Text> <View style={styles.roomTitle}>{ getRoomTitle(room) }</View>
</View> </View>
{!this.isDirect() && this.renderItem('description', room)} {!this.isDirect() && this.renderItem('description', room)}
{!this.isDirect() && this.renderItem('topic', room)} {!this.isDirect() && this.renderItem('topic', room)}

View File

@ -31,7 +31,8 @@ export default StyleSheet.create({
}, },
roomTitle: { roomTitle: {
fontSize: 18, fontSize: 18,
paddingTop: 20 paddingTop: 20,
flexDirection: 'row'
}, },
roomDescription: { roomDescription: {
fontSize: 14, fontSize: 14,

View File

@ -39,6 +39,7 @@ export default class MentionedMessagesView extends LoggedView {
accessibilityLabel={label} accessibilityLabel={label}
accessibilityTraits='button' accessibilityTraits='button'
style={styles.headerButtonTouchable} style={styles.headerButtonTouchable}
testID='room-members-view-toggle-status'
> >
<View style={styles.headerButton}> <View style={styles.headerButton}>
<Text style={styles.headerButtonText}>{label}</Text> <Text style={styles.headerButtonText}>{label}</Text>
@ -168,6 +169,7 @@ export default class MentionedMessagesView extends LoggedView {
blurOnSubmit blurOnSubmit
autoCorrect={false} autoCorrect={false}
autoCapitalize='none' autoCapitalize='none'
testID='room-members-view-search'
/> />
</View> </View>
) )
@ -185,6 +187,7 @@ export default class MentionedMessagesView extends LoggedView {
showLastMessage={false} showLastMessage={false}
avatarSize={30} avatarSize={30}
statusStyle={styles.status} statusStyle={styles.status}
testID={`room-members-view-item-${ item.username }`}
/> />
) )
@ -194,6 +197,7 @@ export default class MentionedMessagesView extends LoggedView {
[ [
<FlatList <FlatList
key='room-members-view-list' key='room-members-view-list'
testID='room-members-view'
data={filtering ? membersFiltered : members} data={filtering ? membersFiltered : members}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}

View File

@ -136,7 +136,8 @@ export default class RoomHeaderView extends React.PureComponent {
style={styles.titleContainer} style={styles.titleContainer}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
accessibilityTraits='header' accessibilityTraits='header'
onPress={() => this.props.navigation.navigate({ key: 'RoomInfo', routeName: 'RoomInfo', params: this.state.room })} onPress={() => this.props.navigation.navigate({ key: 'RoomInfo', routeName: 'RoomInfo', params: { rid: this.state.room.rid } })}
testID='room-view-header-title'
> >
<Avatar <Avatar
@ -151,10 +152,12 @@ export default class RoomHeaderView extends React.PureComponent {
} }
</Avatar> </Avatar>
<View style={styles.titleTextContainer}> <View style={styles.titleTextContainer}>
<Text style={styles.title} allowFontScaling={false}> <View style={{ flexDirection: 'row' }}>
<RoomTypeIcon type={this.state.room.t} size={13} />&nbsp; <RoomTypeIcon type={this.state.room.t} size={13} />
<Text style={styles.title} allowFontScaling={false} testID='room-view-title'>
{this.state.room.name} {this.state.room.name}
</Text> </Text>
</View>
{ t && <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text>} { t && <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text>}
@ -176,6 +179,7 @@ export default class RoomHeaderView extends React.PureComponent {
}} }}
accessibilityLabel='Star room' accessibilityLabel='Star room'
accessibilityTraits='button' accessibilityTraits='button'
testID='room-view-header-star'
> >
<Icon <Icon
name={`${ Platform.OS === 'ios' ? 'ios' : 'md' }-star${ this.state.room.f ? '' : '-outline' }`} name={`${ Platform.OS === 'ios' ? 'ios' : 'md' }-star${ this.state.room.f ? '' : '-outline' }`}
@ -189,6 +193,7 @@ export default class RoomHeaderView extends React.PureComponent {
onPress={() => this.props.navigation.navigate({ key: 'RoomActions', routeName: 'RoomActions', params: { rid: this.state.room.rid } })} onPress={() => this.props.navigation.navigate({ key: 'RoomActions', routeName: 'RoomActions', params: { rid: this.state.room.rid } })}
accessibilityLabel='Room actions' accessibilityLabel='Room actions'
accessibilityTraits='button' accessibilityTraits='button'
testID='room-view-header-actions'
> >
<Icon <Icon
name={Platform.OS === 'ios' ? 'ios-more' : 'md-more'} name={Platform.OS === 'ios' ? 'ios-more' : 'md-more'}
@ -202,7 +207,7 @@ export default class RoomHeaderView extends React.PureComponent {
render() { render() {
return ( return (
<View style={styles.header}> <View style={styles.header} testID='room-view-header'>
{this.renderLeft()} {this.renderLeft()}
{this.renderCenter()} {this.renderCenter()}
{this.renderRight()} {this.renderRight()}

View File

@ -77,6 +77,7 @@ export class List extends React.Component {
renderRow={(item, previousItem) => this.props.renderRow(item, previousItem)} renderRow={(item, previousItem) => this.props.renderRow(item, previousItem)}
initialListSize={20} initialListSize={20}
pageSize={20} pageSize={20}
testID='room-view-messages'
{...scrollPersistTaps} {...scrollPersistTaps}
/>); />);
} }

View File

@ -48,7 +48,10 @@ export default class ReactionPicker extends React.Component {
animationIn='fadeIn' animationIn='fadeIn'
animationOut='fadeOut' animationOut='fadeOut'
> >
<View style={[styles.reactionPickerContainer, { width: width - margin, height: Math.min(width, height) - (margin * 2) }]}> <View
style={[styles.reactionPickerContainer, { width: width - margin, height: Math.min(width, height) - (margin * 2) }]}
testID='reaction-picker'
>
<EmojiPicker <EmojiPicker
tabEmojiStyle={tabEmojiStyle} tabEmojiStyle={tabEmojiStyle}
width={Math.min(width, height) - margin} width={Math.min(width, height) - margin}

View File

@ -74,8 +74,8 @@ export default class RoomView extends LoggedView {
this.onReactionPress = this.onReactionPress.bind(this); this.onReactionPress = this.onReactionPress.bind(this);
} }
async componentDidMount() { componentDidMount() {
await this.updateRoom(); this.updateRoom();
this.rooms.addListener(this.updateRoom); this.rooms.addListener(this.updateRoom);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
@ -197,7 +197,7 @@ export default class RoomView extends LoggedView {
} }
render() { render() {
return ( return (
<View style={styles.container}> <View style={styles.container} testID='room-view'>
<List <List
key='room-view-messages' key='room-view-messages'
end={this.state.end} end={this.state.end}

View File

@ -121,7 +121,13 @@ export default class RoomsListHeaderView extends React.PureComponent {
} }
return ( return (
<View style={styles.left} accessible accessibilityLabel="Server's list" accessibilityTraits='button'> <View
style={styles.left}
accessible
accessibilityLabel={`Connected to ${ this.props.baseUrl }. Tap to view servers list.`}
accessibilityTraits='button'
testID='rooms-list-view-sidebar'
>
<TouchableOpacity <TouchableOpacity
style={styles.headerButton} style={styles.headerButton}
onPress={() => this.props.navigation.openDrawer()} onPress={() => this.props.navigation.openDrawer()}
@ -150,10 +156,15 @@ export default class RoomsListHeaderView extends React.PureComponent {
const t = title(offline, connecting, authenticating, logged); const t = title(offline, connecting, authenticating, logged);
const accessibilityLabel = `${ user.username }, ${ this.getUserStatusLabel() }, double tap to change status`; const accessibilityLabel = `${ user.username }, ${ this.getUserStatusLabel() }, tap to change status`;
return ( return (
<TouchableOpacity
<TouchableOpacity style={styles.titleContainer} onPress={() => this.showModal()} accessibilityLabel={accessibilityLabel} accessibilityTraits='header'> style={styles.titleContainer}
onPress={() => this.showModal()}
accessibilityLabel={accessibilityLabel}
accessibilityTraits='header'
testID='rooms-list-view-user'
>
<Avatar <Avatar
text={user.username} text={user.username}
size={24} size={24}
@ -195,6 +206,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
onPress={() => this.createChannel()} onPress={() => this.createChannel()}
accessibilityLabel='Create channel' accessibilityLabel='Create channel'
accessibilityTraits='button' accessibilityTraits='button'
testID='rooms-list-view-create-channel'
> >
<Icon <Icon
name='ios-add' name='ios-add'
@ -215,6 +227,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
<TouchableOpacity <TouchableOpacity
style={styles.modalButton} style={styles.modalButton}
onPress={() => this.onPressModalButton(status)} onPress={() => this.onPressModalButton(status)}
testID={`rooms-list-view-user-presence-${ status }`}
> >
<View style={statusStyle} /> <View style={statusStyle} />
<Text style={textStyle}> <Text style={textStyle}>
@ -251,7 +264,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
render() { render() {
return ( return (
<View style={styles.header}> <View style={styles.header} testID='rooms-list-view-header'>
{this.renderLeft()} {this.renderLeft()}
{this.renderCenter()} {this.renderCenter()}
{this.renderRight()} {this.renderRight()}
@ -262,6 +275,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
style={{ alignItems: 'center' }} style={{ alignItems: 'center' }}
onModalHide={() => this.hideModal()} onModalHide={() => this.hideModal()}
onBackdropPress={() => this.hideModal()} onBackdropPress={() => this.hideModal()}
testID='rooms-list-view-user-presence-modal'
> >
<View style={styles.modal}> <View style={styles.modal}>
{this.renderModalButton('online')} {this.renderModalButton('online')}

View File

@ -158,6 +158,7 @@ export default class RoomsListView extends LoggedView {
blurOnSubmit blurOnSubmit
autoCorrect={false} autoCorrect={false}
autoCapitalize='none' autoCapitalize='none'
testID='rooms-list-view-search'
/> />
</View> </View>
); );
@ -177,6 +178,7 @@ export default class RoomsListView extends LoggedView {
type={item.t} type={item.t}
baseUrl={this.props.Site_Url} baseUrl={this.props.Site_Url}
onPress={() => this._onPressItem(item)} onPress={() => this._onPressItem(item)}
testID={`rooms-list-view-item-${ item.name }`}
/>); />);
} }
@ -191,6 +193,7 @@ export default class RoomsListView extends LoggedView {
enableEmptySections enableEmptySections
removeClippedSubviews removeClippedSubviews
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
testID='rooms-list-view-list'
/> />
) )
@ -203,7 +206,7 @@ export default class RoomsListView extends LoggedView {
); );
render = () => ( render = () => (
<View style={styles.container}> <View style={styles.container} testID='rooms-list-view'>
{this.renderList()} {this.renderList()}
{Platform.OS === 'android' && this.renderCreateButtons()} {Platform.OS === 'android' && this.renderCreateButtons()}
</View>) </View>)

View File

@ -109,6 +109,7 @@ export default class SearchMessagesView extends LoggedView {
return ( return (
<View <View
style={styles.container} style={styles.container}
testID='search-messages-view'
> >
<View style={styles.searchContainer}> <View style={styles.searchContainer}>
<RCTextInput <RCTextInput
@ -116,6 +117,7 @@ export default class SearchMessagesView extends LoggedView {
label='Search' label='Search'
onChangeText={this.onChangeSearch} onChangeText={this.onChangeSearch}
placeholder='Search Messages' placeholder='Search Messages'
testID='search-message-view-input'
/> />
<Markdown msg='You can search using RegExp. e.g. `/^text$/i`' /> <Markdown msg='You can search using RegExp. e.g. `/^text$/i`' />
<View style={styles.divider} /> <View style={styles.divider} />

View File

@ -103,6 +103,7 @@ export default class SelectedUsersView extends LoggedView {
onPress={() => params.nextAction()} onPress={() => params.nextAction()}
accessibilityLabel='Submit' accessibilityLabel='Submit'
accessibilityTraits='button' accessibilityTraits='button'
testID='selected-users-view-submit'
> >
<Icon <Icon
name='ios-add' name='ios-add'
@ -229,6 +230,7 @@ export default class SelectedUsersView extends LoggedView {
placeholder='Search' placeholder='Search'
clearButtonMode='while-editing' clearButtonMode='while-editing'
blurOnSubmit blurOnSubmit
testID='select-users-view-search'
autoCorrect={false} autoCorrect={false}
autoCapitalize='none' autoCapitalize='none'
/> />
@ -255,6 +257,7 @@ export default class SelectedUsersView extends LoggedView {
key={item._id} key={item._id}
style={styles.selectItemView} style={styles.selectItemView}
onPress={() => this._onPressSelectedItem(item)} onPress={() => this._onPressSelectedItem(item)}
testID={`selected-user-${ item.name }`}
> >
<Avatar text={item.name} size={40} /> <Avatar text={item.name} size={40} />
<Text ellipsizeMode='tail' numberOfLines={1} style={{ fontSize: 10 }}> <Text ellipsizeMode='tail' numberOfLines={1} style={{ fontSize: 10 }}>
@ -273,6 +276,7 @@ export default class SelectedUsersView extends LoggedView {
showLastMessage={false} showLastMessage={false}
avatarSize={30} avatarSize={30}
statusStyle={styles.status} statusStyle={styles.status}
testID={`select-users-view-item-${ item.name }`}
/> />
); );
renderList = () => ( renderList = () => (
@ -300,7 +304,7 @@ export default class SelectedUsersView extends LoggedView {
); );
}; };
render = () => ( render = () => (
<View style={styles.container}> <View style={styles.container} testID='select-users-view'>
<SafeAreaView style={styles.safeAreaView}> <SafeAreaView style={styles.safeAreaView}>
{this.renderList()} {this.renderList()}
{this.renderCreateButton()} {this.renderCreateButton()}

View File

@ -73,7 +73,7 @@ export default class SnippetedMessagesView extends LoggedView {
} }
renderEmpty = () => ( renderEmpty = () => (
<View style={styles.listEmptyContainer}> <View style={styles.listEmptyContainer} testID='snippeted-messages-view'>
<Text>No snippeted messages</Text> <Text>No snippeted messages</Text>
</View> </View>
) )
@ -99,8 +99,10 @@ export default class SnippetedMessagesView extends LoggedView {
} }
return ( return (
[
<FlatList <FlatList
key='snippet-messages-view-list' key='snippeted-messages-view-list'
testID='snippeted-messages-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
@ -109,6 +111,7 @@ export default class SnippetedMessagesView extends LoggedView {
ListHeaderComponent={loading && <RCActivityIndicator />} ListHeaderComponent={loading && <RCActivityIndicator />}
ListFooterComponent={loadingMore && <RCActivityIndicator />} ListFooterComponent={loadingMore && <RCActivityIndicator />}
/> />
]
); );
} }
} }

View File

@ -97,7 +97,7 @@ export default class StarredMessagesView extends LoggedView {
} }
renderEmpty = () => ( renderEmpty = () => (
<View style={styles.listEmptyContainer}> <View style={styles.listEmptyContainer} testID='starred-messages-view'>
<Text>No starred messages</Text> <Text>No starred messages</Text>
</View> </View>
) )
@ -126,6 +126,7 @@ export default class StarredMessagesView extends LoggedView {
[ [
<FlatList <FlatList
key='starred-messages-view-list' key='starred-messages-view-list'
testID='starred-messages-view'
data={messages} data={messages}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}

50
e2e/00-addserver.spec.js Normal file
View File

@ -0,0 +1,50 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
describe('Add server', () => {
before(async() => {
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
});
describe('Render', async() => {
it('should have add server screen', async() => {
await expect(element(by.id('new-server-view'))).toBeVisible();
});
it('should have server input', async() => {
await expect(element(by.id('new-server-view-input'))).toBeVisible();
});
it('should have submit button', async() => {
await expect(element(by.id('new-server-view-button'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
it('should insert "invalidtest" and get an invalid instance', async() => {
await element(by.id('new-server-view-input')).replaceText('invalidtest');
await waitFor(element(by.text(' is not a valid Rocket.Chat instance'))).toBeVisible().withTimeout(60000);
await expect(element(by.text(' is not a valid Rocket.Chat instance'))).toBeVisible();
});
it('should have a button to add a new server', async() => {
await element(by.id('new-server-view-input')).replaceText(data.server);
await waitFor(element(by.text(' is a valid Rocket.Chat instance'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('new-server-view-button'))).toBeVisible();
await element(by.id('new-server-view-button')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('welcome-view'))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
});

46
e2e/01-welcome.spec.js Normal file
View File

@ -0,0 +1,46 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
describe('Welcome screen', () => {
describe('Render', async() => {
it('should have welcome screen', async() => {
await expect(element(by.id('welcome-view'))).toBeVisible();
});
it('should have register button', async() => {
await expect(element(by.id('welcome-view-register'))).toBeVisible();
});
it('should have login button', async() => {
await expect(element(by.id('welcome-view-login'))).toBeVisible();
});
// TODO: oauth
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
it('should navigate to login', async() => {
await element(by.id('welcome-view-login')).tap();
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('login-view'))).toBeVisible();
await element(by.id('close-modal-button')).tap();
});
it('should navigate to register', async() => {
await element(by.id('welcome-view-register')).tap();
await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('register-view'))).toBeVisible();
await element(by.id('close-modal-button')).tap();
});
afterEach(async() => {
takeScreenshot();
});
});
});

View File

@ -0,0 +1,45 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
describe('Forgot password screen', () => {
before(async() => {
await element(by.id('welcome-view-login')).tap();
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000);
await element(by.id('login-view-forgot-password')).tap();
await waitFor(element(by.id('forgot-password-view'))).toBeVisible().withTimeout(2000);
});
describe('Render', async() => {
it('should have forgot password screen', async() => {
await expect(element(by.id('forgot-password-view'))).toBeVisible();
});
it('should have email input', async() => {
await expect(element(by.id('forgot-password-view-email'))).toBeVisible();
});
it('should have submit button', async() => {
await expect(element(by.id('forgot-password-view-submit'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
it('should reset password and navigate to login', async() => {
await element(by.id('forgot-password-view-email')).replaceText(data.email);
await element(by.id('forgot-password-view-submit')).tap();
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('login-view'))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
});

148
e2e/03-createuser.spec.js Normal file
View File

@ -0,0 +1,148 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const { logout } = require('./helpers/app');
const data = require('./data');
async function navigateToRegister() {
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000);
await element(by.id('welcome-view-register')).tap();
await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000);
}
describe('Create user screen', () => {
before(async() => {
await device.reloadReactNative();
await navigateToRegister();
});
describe('Render', () => {
it('should have create user screen', async() => {
await expect(element(by.id('register-view'))).toBeVisible();
});
it('should have name input', async() => {
await expect(element(by.id('register-view-name'))).toBeVisible();
});
it('should have email input', async() => {
await expect(element(by.id('register-view-email'))).toBeVisible();
});
it('should have password input', async() => {
await expect(element(by.id('register-view-password'))).toBeVisible();
});
it('should have show password icon', async() => {
await expect(element(by.id('register-view-password-icon-right'))).toBeVisible();
});
it('should have repeat password input', async() => {
await expect(element(by.id('register-view-repeat-password'))).toBeVisible();
});
it('should have repeat password icon', async() => {
await expect(element(by.id('register-view-repeat-password-icon-right'))).toBeVisible();
});
it('should have submit button', async() => {
await expect(element(by.id('register-view-submit'))).toBeVisible();
});
it('should have close modal', async() => {
await expect(element(by.id('close-modal-button'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', () => {
it('should navigate to welcome', async() => {
await element(by.id('close-modal-button')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('welcome-view'))).toBeVisible();
});
it('should submit empty form and raise error', async() => {
await navigateToRegister();
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Some field is invalid or empty'))).toBeVisible().withTimeout(10000);
await expect(element(by.text('Some field is invalid or empty'))).toBeVisible();
});
it('should submit different passwords and raise error', async() => {
await element(by.id('register-view-name')).replaceText(data.user);
await element(by.id('register-view-email')).replaceText(data.email);
await element(by.id('register-view-password')).replaceText('abc');
await element(by.id('register-view-repeat-password')).replaceText('xyz');
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Some field is invalid or empty'))).toBeVisible().withTimeout(10000);
await expect(element(by.text('Some field is invalid or empty'))).toBeVisible();
});
it('should submit invalid email and raise error', async() => {
await element(by.id('register-view-name')).replaceText(data.user);
await element(by.id('register-view-email')).replaceText('invalidemail');
await element(by.id('register-view-password')).replaceText(data.password);
await element(by.id('register-view-repeat-password')).replaceText(data.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('register-view-error'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('register-view-error'))).toBeVisible();
await expect(element(by.id('register-view-error'))).toHaveText('Invalid email invalidemail');
});
it('should submit email already taken and raise error', async() => {
await element(by.id('register-view-name')).replaceText(data.user);
await element(by.id('register-view-email')).replaceText('diego.mello@rocket.chat');
await element(by.id('register-view-password')).replaceText(data.password);
await element(by.id('register-view-repeat-password')).replaceText(data.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('register-view-error'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('register-view-error'))).toBeVisible();
await expect(element(by.id('register-view-error'))).toHaveText('Email already exists.');
});
it('should complete first part of register', async() => {
await element(by.id('register-view-name')).replaceText(data.user);
await element(by.id('register-view-email')).replaceText(data.email);
await element(by.id('register-view-password')).replaceText(data.password);
await element(by.id('register-view-repeat-password')).replaceText(data.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('register-view-username'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('register-view-username'))).toBeVisible();
});
it('should submit empty username and raise error', async() => {
await element(by.id('register-view-submit-username')).tap();
await waitFor(element(by.text('Username is empty'))).toBeVisible().withTimeout(10000);
await expect(element(by.text('Username is empty'))).toBeVisible();
});
it('should submit already taken username and raise error', async() => {
await element(by.id('register-view-username')).replaceText('diego.mello');
await element(by.id('register-view-submit-username')).tap();
await waitFor(element(by.id('register-view-error'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('register-view-error'))).toBeVisible();
});
it('should finish register', async() => {
await element(by.id('register-view-username')).replaceText(data.user);
await element(by.id('register-view-submit-username')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
});
// TODO: terms and privacy
afterEach(async() => {
takeScreenshot();
});
after(async() => {
await logout();
});
});
});

92
e2e/04-login.spec.js Normal file
View File

@ -0,0 +1,92 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const { navigateToLogin } = require('./helpers/app');
const data = require('./data');
describe('Login screen', () => {
before(async() => {
await navigateToLogin();
});
describe('Render', () => {
it('should have login screen', async() => {
await expect(element(by.id('login-view'))).toBeVisible();
});
it('should have email input', async() => {
await expect(element(by.id('login-view-email'))).toBeVisible();
});
it('should have password input', async() => {
await expect(element(by.id('login-view-password'))).toBeVisible();
});
it('should have show password icon', async() => {
await expect(element(by.id('login-view-password-icon-right'))).toBeVisible();
});
it('should have submit button', async() => {
await expect(element(by.id('login-view-submit'))).toBeVisible();
});
it('should have register button', async() => {
await expect(element(by.id('login-view-register'))).toBeVisible();
});
it('should have forgot password button', async() => {
await expect(element(by.id('login-view-forgot-password'))).toBeVisible();
});
it('should have close modal button', async() => {
await expect(element(by.id('close-modal-button'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', () => {
it('should navigate to register', async() => {
await element(by.id('login-view-register')).tap();
await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('register-view'))).toBeVisible();
await element(by.id('close-modal-button').withAncestor(by.id('register-view'))).tap();
});
it('should navigate to forgot password', async() => {
await element(by.id('login-view-forgot-password')).tap();
await waitFor(element(by.id('forgot-password-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('forgot-password-view'))).toBeVisible();
await element(by.id('header-back')).tap();
});
it('should navigate to welcome', async() => {
await element(by.id('close-modal-button')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('welcome-view'))).toBeVisible();
await navigateToLogin();
});
it('should insert wrong password and get error', async() => {
await element(by.id('login-view-email')).replaceText(data.user);
await element(by.id('login-view-password')).replaceText('error');
await element(by.id('login-view-submit')).tap();
await waitFor(element(by.text('User or Password incorrect'))).toBeVisible().withTimeout(10000);
await expect(element(by.text('User or Password incorrect'))).toBeVisible();
});
it('should login with success', async() => {
await element(by.id('login-view-password')).replaceText(data.password);
await element(by.id('login-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
});

112
e2e/05-roomslist.spec.js Normal file
View File

@ -0,0 +1,112 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const { login, navigateToLogin } = require('./helpers/app');
const data = require('./data');
describe('Rooms list screen', () => {
before(async() => {
await device.reloadReactNative(); // TODO: remove this after fix logout subscription
});
describe('Render', async() => {
it('should have rooms list screen', async() => {
await expect(element(by.id('rooms-list-view'))).toBeVisible();
});
it('should have rooms list', async() => {
await expect(element(by.id('rooms-list-view-list'))).toBeVisible();
});
it('should have room item', async() => {
await expect(element(by.id('rooms-list-view-item-general'))).toExist();
});
// Render - Header
describe('Header', async() => {
it('should have header', async() => {
await expect(element(by.id('rooms-list-view-header'))).toBeVisible();
});
it('should have create channel button', async() => {
await expect(element(by.id('rooms-list-view-create-channel'))).toBeVisible();
});
it('should have user', async() => {
await expect(element(by.id('rooms-list-view-user'))).toBeVisible();
});
it('should have sidebar button', async() => {
await expect(element(by.id('rooms-list-view-sidebar'))).toBeVisible();
await expect(element(by.id('rooms-list-view-sidebar'))).toHaveLabel(`Connected to ${ data.server }. Tap to view servers list.`);
});
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
it('should change user presence modal', async() => {
await waitFor(element(by.label(`${ data.user }, Online, tap to change status`))).toBeVisible().withTimeout(60000);
await expect(element(by.label(`${ data.user }, Online, tap to change status`))).toBeVisible();
await element(by.id('rooms-list-view-user')).tap();
await waitFor(element(by.id('rooms-list-view-user-presence-modal'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('rooms-list-view-user-presence-modal'))).toBeVisible();
await element(by.id('rooms-list-view-user-presence-busy')).tap();
await waitFor(element(by.id('rooms-list-view-user-presence-modal'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.id('rooms-list-view-user-presence-modal'))).toBeNotVisible();
await waitFor(element(by.label(`${ data.user }, Busy, tap to change status`))).toBeVisible().withTimeout(60000);
await expect(element(by.label(`${ data.user }, Busy, tap to change status`))).toBeVisible();
});
it('should search room and navigate', async() => {
await element(by.id('rooms-list-view-list')).swipe('down');
await waitFor(element(by.id('rooms-list-view-search'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('rooms-list-view-search'))).toBeVisible();
await element(by.id('rooms-list-view-search')).replaceText('rocket.cat');
await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible().withTimeout(10000);
await expect(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible();
await element(by.id('rooms-list-view-item-rocket.cat')).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(10000);
await expect(element(by.id('room-view'))).toBeVisible();
await waitFor(element(by.id('room-view-title'))).toHaveText('rocket.cat').withTimeout(60000);
await expect(element(by.id('room-view-title'))).toHaveText('rocket.cat');
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible();
});
// Usage - Sidebar
describe('Sidebar', async() => {
it('should navigate to add server', async() => {
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
await element(by.id('sidebar-add-server')).tap();
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('new-server-view'))).toBeVisible();
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
});
it('should logout', async() => {
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
await element(by.id('sidebar-logout')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('welcome-view'))).toBeVisible();
await navigateToLogin();
await login();
});
});
afterEach(async() => {
takeScreenshot();
});
});
});

109
e2e/06-createroom.spec.js Normal file
View File

@ -0,0 +1,109 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
describe('Create room screen', () => {
before(async() => {
await device.reloadReactNative(); // TODO: remove this after fix logout subscription
await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000);
});
describe('Render', async() => {
it('should have select users screen', async() => {
await expect(element(by.id('select-users-view'))).toBeVisible();
});
it('should have search input', async() => {
await expect(element(by.id('select-users-view-search'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
it('should back to rooms list', async() => {
await element(by.id('header-back')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000);
});
it('should search users', async() => {
await element(by.id('select-users-view-search')).replaceText('rocket.cat');
await waitFor(element(by.id(`select-users-view-item-rocket.cat`))).toBeVisible().withTimeout(10000);
await expect(element(by.id(`select-users-view-item-rocket.cat`))).toBeVisible();
});
it('should select/unselect user', async() => {
await element(by.id('select-users-view-item-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(5000);
await expect(element(by.id('selected-user-rocket.cat'))).toBeVisible();
await expect(element(by.id('selected-users-view-submit'))).toBeVisible();
await element(by.id('selected-user-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat'))).toBeNotVisible().withTimeout(5000);
await expect(element(by.id('selected-user-rocket.cat'))).toBeNotVisible();
await expect(element(by.id('selected-users-view-submit'))).toBeNotVisible();
await element(by.id('select-users-view-item-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(5000);
});
it('should navigate to create channel view', async() => {
await element(by.id('selected-users-view-submit')).tap();
await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000);
await expect(element(by.id('create-channel-view'))).toBeVisible();
await expect(element(by.id('create-channel-name'))).toBeVisible();
await expect(element(by.id('create-channel-type'))).toBeVisible();
await expect(element(by.id('create-channel-submit'))).toBeVisible();
});
it('should get invalid room', async() => {
await element(by.id('create-channel-name')).replaceText('general');
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('create-channel-error'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('create-channel-error'))).toBeVisible();
});
it('should create private room', async() => {
await element(by.id('create-channel-name')).replaceText(`private${ data.random }`);
await element(by.id('create-channel-type')).tap();
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('room-view'))).toBeVisible();
await waitFor(element(by.id('room-view-title'))).toHaveText(`private${ data.random }`).withTimeout(60000);
await expect(element(by.id('room-view-title'))).toHaveText(`private${ data.random }`);
await element(by.id('header-back')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible();
});
it('should create public room', async() => {
await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000);
await element(by.id('select-users-view-item-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(5000);
await element(by.id('selected-users-view-submit')).tap();
await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000);
await element(by.id('create-channel-name')).replaceText(`public${ data.random }`);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('room-view'))).toBeVisible();
await waitFor(element(by.id('room-view-title'))).toHaveText(`public${ data.random }`).withTimeout(60000);
await expect(element(by.id('room-view-title'))).toHaveText(`public${ data.random }`);
await element(by.id('header-back')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
});

318
e2e/07-room.spec.js Normal file
View File

@ -0,0 +1,318 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
async function mockMessage(message) {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText(`${ data.random }${ message }`);
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.text(`${ data.random }${ message }`))).toBeVisible().withTimeout(60000);
};
async function navigateToRoom() {
await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible().withTimeout(60000);
await element(by.id(`rooms-list-view-item-private${ data.random }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
}
describe('Room screen', () => {
before(async() => {
await navigateToRoom();
});
describe('Render', async() => {
it('should have room screen', async() => {
await expect(element(by.id('room-view'))).toBeVisible();
});
it('should have messages list', async() => {
await expect(element(by.id('room-view-messages'))).toBeVisible();
});
// Render - Header
describe('Header', async() => {
it('should have room header', async() => {
await expect(element(by.id('room-view-header'))).toBeVisible();
});
it('should have back button', async() => {
await expect(element(by.id('header-back'))).toBeVisible();
});
it('should have title', async() => {
await expect(element(by.id('room-view-header-title'))).toBeVisible();
await expect(element(by.id('room-view-title'))).toHaveText(`private${ data.random }`);
});
it('should have star button', async() => {
await expect(element(by.id('room-view-header-star'))).toBeVisible();
});
it('should have actions button ', async() => {
await expect(element(by.id('room-view-header-actions'))).toBeVisible();
});
});
// Render - Messagebox
describe('Messagebox', async() => {
it('should have messagebox', async() => {
await expect(element(by.id('messagebox'))).toBeVisible();
});
it('should have open emoji button', async() => {
await expect(element(by.id('messagebox-open-emoji'))).toBeVisible();
});
it('should have message input', async() => {
await expect(element(by.id('messagebox-input'))).toBeVisible();
});
it('should have audio button', async() => {
await expect(element(by.id('messagebox-send-audio'))).toBeVisible();
});
it('should have actions button', async() => {
await expect(element(by.id('messagebox-actions'))).toBeVisible();
});
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
describe('Header', async() => {
it('should back to rooms list', async() => {
await element(by.id('header-back')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await navigateToRoom();
});
it('should tap on title and navigate to room info', async() => {
await element(by.id('room-view-header-title')).tap();
await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('room-info-view'))).toBeVisible();
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
});
it('should tap on more and navigate to room actions', async() => {
await element(by.id('room-view-header-actions')).tap();
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('room-actions-view'))).toBeVisible();
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
});
});
describe('Messagebox', async() => {
it('should send message', async() => {
await mockMessage('message');
await waitFor(element(by.text(`${ data.random }message`))).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }message`))).toBeVisible();
});
it('should show/hide emoji keyboard', async() => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji'))).toBeVisible().withTimeout(10000);
await expect(element(by.id('messagebox-keyboard-emoji'))).toBeVisible();
await expect(element(by.id('messagebox-close-emoji'))).toBeVisible();
await expect(element(by.id('messagebox-open-emoji'))).toBeNotVisible();
await element(by.id('messagebox-close-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.id('messagebox-keyboard-emoji'))).toBeNotVisible();
await expect(element(by.id('messagebox-close-emoji'))).toBeNotVisible();
await expect(element(by.id('messagebox-open-emoji'))).toBeVisible();
});
it('should show/hide emoji autocomplete', async() => {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).replaceText(':');
await element(by.id('messagebox-input')).typeText('joy'); // workaround for number keyboard
await waitFor(element(by.id('messagebox-container'))).toBeVisible().withTimeout(10000);
await expect(element(by.id('messagebox-container'))).toBeVisible();
await element(by.id('messagebox-input')).clearText();
await waitFor(element(by.id('messagebox-container'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.id('messagebox-container'))).toBeNotVisible();
});
it('should show and tap on emoji autocomplete', async() => {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).replaceText(':');
await element(by.id('messagebox-input')).typeText('joy'); // workaround for number keyboard
await waitFor(element(by.id('messagebox-container'))).toBeVisible().withTimeout(10000);
await expect(element(by.id('messagebox-container'))).toBeVisible();
await element(by.id('mention-item-joy')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText(':joy: ');
await element(by.id('messagebox-input')).clearText();
});
it('should show and tap on user autocomplete and send mention', async() => {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText(`@${ data.user }`);
await waitFor(element(by.id('messagebox-container'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('messagebox-container'))).toBeVisible();
await element(by.id(`mention-item-${ data.user }`)).tap();
await expect(element(by.id('messagebox-input'))).toHaveText(`@${ data.user } `);
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText('test');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.text(`@${ data.user } test`))).toBeVisible().withTimeout(60000);
});
it('should show and tap on room autocomplete', async() => {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText('#general');
await waitFor(element(by.id('messagebox-container'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('messagebox-container'))).toBeVisible();
await element(by.id('mention-item-general')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('#general ');
await element(by.id('messagebox-input')).clearText();
});
});
describe('Message', async() => {
before(async() => {
await mockMessage('reply');
await mockMessage('edit');
await mockMessage('quote');
});
it('should show message actions', async() => {
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Cancel')).tap();
await waitFor(element(by.text('Cancel'))).toBeNotVisible().withTimeout(2000);
});
it('should reply message', async() => {
await element(by.text(`${ data.random }reply`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Reply')).tap();
await element(by.id('messagebox-input')).typeText('replied');
await element(by.id('messagebox-send-message')).tap();
// TODO: test if reply was sent
});
it('should edit message', async() => {
await element(by.text(`${ data.random }edit`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Edit')).tap();
await element(by.id('messagebox-input')).typeText('ed');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.text(`${ data.random }edited`))).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited`))).toBeVisible();
});
it('should copy permalink', async() => {
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Copy Permalink')).tap();
await expect(element(by.text('Permalink copied to clipboard!'))).toBeVisible();
await waitFor(element(by.text('Permalink copied to clipboard!'))).toBeNotVisible().withTimeout(5000);
// TODO: test clipboard
});
it('should copy message', async() => {
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Copy Message')).tap();
await expect(element(by.text('Copied to clipboard!'))).toBeVisible();
await waitFor(element(by.text('Copied to clipboard!'))).toBeNotVisible().withTimeout(5000);
// TODO: test clipboard
});
it('should quote message', async() => {
await element(by.text(`${ data.random }quote`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Quote')).tap();
await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`);
await element(by.id('messagebox-send-message')).tap();
// TODO: test if quote was sent
});
it('should star message', async() => {
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Star')).tap();
await waitFor(element(by.text('Messages actions'))).toBeNotVisible().withTimeout(5000);
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Unstar'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Unstar'))).toBeVisible();
await element(by.text('Cancel')).tap();
await waitFor(element(by.text('Cancel'))).toBeNotVisible().withTimeout(2000);
});
it('should react to message', async() => {
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Add Reaction')).tap();
await waitFor(element(by.id('reaction-picker'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('reaction-picker'))).toBeVisible();
await element(by.id('reaction-picker-😃')).tap();
await waitFor(element(by.id('reaction-picker-grinning'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('reaction-picker-grinning'))).toBeVisible();
await element(by.id('reaction-picker-grinning')).tap();
await waitFor(element(by.id('message-reaction-:grinning:'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('message-reaction-:grinning:'))).toBeVisible();
});
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async() => {
await element(by.id('message-add-reaction')).tap();
await waitFor(element(by.id('reaction-picker'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('reaction-picker'))).toBeVisible();
await waitFor(element(by.id('reaction-picker-grinning'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('reaction-picker-grinning'))).toBeVisible();
await element(by.id('reaction-picker-😃')).tap();
await waitFor(element(by.id('reaction-picker-grimacing'))).toBeVisible().withTimeout(2000);
await element(by.id('reaction-picker-grimacing')).tap();
await waitFor(element(by.id('message-reaction-:grimacing:'))).toBeVisible().withTimeout(60000);
});
it('should remove reaction', async() => {
await element(by.id('message-reaction-:grinning:')).tap();
await waitFor(element(by.id('message-reaction-:grinning:'))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id('message-reaction-:grinning:'))).toBeNotVisible();
});
it('should pin message', async() => {
await element(by.text(`${ data.random }edited`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Messages actions'))).toBeVisible();
await element(by.text('Pin')).tap();
await waitFor(element(by.text('Messages actions'))).toBeNotVisible().withTimeout(5000);
await waitFor(element(by.text(`${ data.random }edited`)).atIndex(1)).toBeVisible().withTimeout(60000);
await element(by.text(`${ data.random }edited`)).atIndex(0).longPress();
await waitFor(element(by.text('Unpin'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Unpin'))).toBeVisible();
await element(by.text('Cancel')).tap();
await waitFor(element(by.text('Cancel'))).toBeNotVisible().withTimeout(2000);
});
// TODO: delete message - swipe on action sheet missing
});
afterEach(async() => {
takeScreenshot();
});
after(async() => {
await element(by.id('header-back')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
});
});
});

403
e2e/08-roomactions.spec.js Normal file
View File

@ -0,0 +1,403 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
const scrollDown = 200;
async function navigateToRoomActions(type) {
let room;
if (type === 'd') {
room = 'rocket.cat';
} else {
room = `private${ data.random }`;
}
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeVisible().withTimeout(2000);
await element(by.id(`rooms-list-view-item-${ room }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
await element(by.id('room-view-header-actions')).tap();
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000);
}
async function backToActions() {
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('rooms-actions-view'))).toBeVisible().withTimeout(2000);
}
async function backToRoomsList() {
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
}
describe('Room actions screen', () => {
describe('Render', async() => {
describe('Direct', async() => {
before(async() => {
await navigateToRoomActions('d');
});
it('should have room actions screen', async() => {
await expect(element(by.id('room-actions-view'))).toBeVisible();
});
it('should have info', async() => {
await expect(element(by.id('room-actions-info'))).toBeVisible();
});
it('should have voice', async() => {
await expect(element(by.id('room-actions-voice'))).toBeVisible();
});
it('should have video', async() => {
await expect(element(by.id('room-actions-video'))).toBeVisible();
});
it('should have files', async() => {
await expect(element(by.id('room-actions-files'))).toBeVisible();
});
it('should have mentions', async() => {
await expect(element(by.id('room-actions-mentioned'))).toBeVisible();
});
it('should have starred', async() => {
await expect(element(by.id('room-actions-starred'))).toBeVisible();
});
it('should have search', async() => {
await expect(element(by.id('room-actions-search'))).toBeVisible();
});
it('should have share', async() => {
await waitFor(element(by.id('room-actions-share'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-share'))).toBeVisible();
});
it('should have pinned', async() => {
await waitFor(element(by.id('room-actions-pinned'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-pinned'))).toBeVisible();
});
it('should have snippeted', async() => {
await waitFor(element(by.id('room-actions-snippeted'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-snippeted'))).toBeVisible();
});
it('should have notifications', async() => {
await waitFor(element(by.id('room-actions-notifications'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-notifications'))).toBeVisible();
});
it('should have block user', async() => {
await waitFor(element(by.id('room-actions-block-user'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-block-user'))).toBeVisible();
});
after(async() => {
await backToRoomsList();
});
});
describe('Channel/Group', async() => {
before(async() => {
await navigateToRoomActions('c');
});
it('should have room actions screen', async() => {
await expect(element(by.id('room-actions-view'))).toBeVisible();
});
it('should have info', async() => {
await expect(element(by.id('room-actions-info'))).toBeVisible();
});
it('should have voice', async() => {
await expect(element(by.id('room-actions-voice'))).toBeVisible();
});
it('should have video', async() => {
await expect(element(by.id('room-actions-video'))).toBeVisible();
});
it('should have members', async() => {
await expect(element(by.id('room-actions-members'))).toBeVisible();
});
it('should have add user', async() => {
await expect(element(by.id('room-actions-add-user'))).toBeVisible();
});
it('should have files', async() => {
await expect(element(by.id('room-actions-files'))).toBeVisible();
});
it('should have mentions', async() => {
await expect(element(by.id('room-actions-mentioned'))).toBeVisible();
});
it('should have starred', async() => {
await expect(element(by.id('room-actions-starred'))).toBeVisible();
});
it('should have search', async() => {
await expect(element(by.id('room-actions-search'))).toBeVisible();
});
it('should have share', async() => {
await waitFor(element(by.id('room-actions-share'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-share'))).toBeVisible();
});
it('should have pinned', async() => {
await waitFor(element(by.id('room-actions-pinned'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-pinned'))).toBeVisible();
});
it('should have snippeted', async() => {
await waitFor(element(by.id('room-actions-snippeted'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-snippeted'))).toBeVisible();
});
it('should have notifications', async() => {
await waitFor(element(by.id('room-actions-notifications'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-notifications'))).toBeVisible();
});
it('should have leave channel', async() => {
await waitFor(element(by.id('room-actions-leave-channel'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-leave-channel'))).toBeVisible();
});
});
afterEach(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
describe('TDB', async() => {
it('should NOT navigate to voice call', async() => {
await waitFor(element(by.id('room-actions-voice'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'up');
await element(by.id('room-actions-voice')).tap();
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('room-actions-view'))).toBeVisible();
});
it('should NOT navigate to video call', async() => {
await element(by.id('room-actions-video')).tap();
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('room-actions-view'))).toBeVisible();
});
it('should NOT navigate to share messages', async() => {
await waitFor(element(by.id('room-actions-share'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await element(by.id('room-actions-share')).tap();
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('room-actions-view'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Common', async() => {
it('should show mentioned messages', async() => {
await element(by.id('room-actions-mentioned')).tap();
await waitFor(element(by.id('mentioned-messages-view'))).toExist().withTimeout(2000);
await waitFor(element(by.text(`@${ data.user } test`).withAncestor(by.id('mentioned-messages-view')))).toBeVisible().withTimeout(60000);
await expect(element(by.text(`@${ data.user } test`).withAncestor(by.id('mentioned-messages-view')))).toBeVisible();
await backToActions();
});
it('should show starred message and unstar it', async() => {
await element(by.id('room-actions-starred')).tap();
await waitFor(element(by.id('starred-messages-view'))).toExist().withTimeout(2000);
await waitFor(element(by.text(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeVisible();
await element(by.text(`${ data.random }message`).withAncestor(by.id('starred-messages-view'))).longPress();
await waitFor(element(by.text('Unstar'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Unstar'))).toBeVisible();
await element(by.text('Unstar')).tap();
await waitFor(element(by.text(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible();
await backToActions();
});
it('should show pinned message and unpin it', async() => {
await waitFor(element(by.id('room-actions-pinned'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await element(by.id('room-actions-pinned')).tap();
await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000);
await waitFor(element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view'))).atIndex(0)).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view')))).toBeVisible();
await element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view'))).longPress();
await waitFor(element(by.text('Unpin'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Unpin'))).toBeVisible();
await element(by.text('Unpin')).tap();
await waitFor(element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view'))).atIndex(0)).toBeNotVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }edited`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible();
await backToActions();
});
it('should search and find a message', async() => {
await element(by.id('room-actions-search')).tap();
await waitFor(element(by.id('search-messages-view'))).toExist().withTimeout(2000);
await expect(element(by.id('search-message-view-input'))).toBeVisible();
await element(by.id('search-message-view-input')).tap();
await element(by.id('search-message-view-input')).replaceText(`/${ data.random }message/`);
await waitFor(element(by.text(`${ data.random }message`).withAncestor(by.id('search-messages-view'))).atIndex(0)).toBeVisible().withTimeout(60000);
await expect(element(by.text(`${ data.random }message`).withAncestor(by.id('search-messages-view'))).atIndex(0)).toBeVisible();
await backToActions();
});
it('should enable/disable notifications', async() => {
await waitFor(element(by.id('room-actions-notifications'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.text('Disable notifications'))).toBeVisible();
await element(by.id('room-actions-notifications')).tap();
await waitFor(element(by.text('Enable notifications'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Enable notifications'))).toBeVisible();
await element(by.id('room-actions-notifications')).tap();
await waitFor(element(by.text('Disable notifications'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Disable notifications'))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
describe('Channel/Group', async() => {
// Currently, there's no way to add more owners to the room
// So we test only for the 'You are the last owner...' message
it('should tap on leave channel and raise alert', async() => {
await waitFor(element(by.id('room-actions-leave-channel'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await expect(element(by.id('room-actions-leave-channel'))).toBeVisible();
await element(by.id('room-actions-leave-channel')).tap();
await waitFor(element(by.text('Yes, leave it!'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Yes, leave it!'))).toBeVisible();
await element(by.text('Yes, leave it!')).tap();
await waitFor(element(by.text('You are the last owner. Please set new owner before leaving the room.'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('You are the last owner. Please set new owner before leaving the room.'))).toBeVisible();
await takeScreenshot();
await element(by.text('OK')).tap();
await waitFor(element(by.id('rooms-actions-view'))).toBeVisible().withTimeout(2000);
});
describe('Add User', async() => {
it('should add user to the room', async() => {
const user = 'detoxrn';
await waitFor(element(by.id('room-actions-add-user'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'up');
await element(by.id('room-actions-add-user')).tap();
await element(by.id('select-users-view-search')).tap();
await element(by.id('select-users-view-search')).replaceText(user);
await waitFor(element(by.id(`select-users-view-item-${ user }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`select-users-view-item-${ user }`))).toBeVisible();
await element(by.id(`select-users-view-item-${ user }`)).tap();
await waitFor(element(by.id(`selected-user-${ user }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`selected-user-${ user }`))).toBeVisible();
await element(by.id('selected-users-view-submit')).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
await element(by.id('room-view-header-actions')).tap();
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000);
await element(by.id('room-actions-members')).tap();
await element(by.id('room-members-view-toggle-status')).tap();
await waitFor(element(by.id(`room-members-view-item-${ user }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user }`))).toBeVisible();
await backToActions();
});
after(async() => {
takeScreenshot();
});
});
describe('Room Members', async() => {
const user = 'detoxrn';
before(async() => {
await element(by.id('room-actions-members')).tap();
await waitFor(element(by.id('room-members-view'))).toExist().withTimeout(2000);
await expect(element(by.id('room-members-view'))).toExist();
});
it('should show/hide all users', async() => {
await element(by.id('room-members-view-toggle-status')).tap();
await waitFor(element(by.id(`room-members-view-item-${ user }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user }`))).toBeVisible();
await element(by.id('room-members-view-toggle-status')).tap();
await waitFor(element(by.id(`room-members-view-item-${ user }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user }`))).toBeNotVisible();
});
it('should filter user', async() => {
await element(by.id('room-members-view-toggle-status')).tap();
await waitFor(element(by.id(`room-members-view-item-${ user }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user }`))).toBeVisible();
await element(by.id('room-members-view-search')).replaceText('rocket');
await waitFor(element(by.id(`room-members-view-item-${ user }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user }`))).toBeNotVisible();
await element(by.id('room-members-view-search')).tap();
await element(by.id('room-members-view-search')).clearText('');
await waitFor(element(by.id(`room-members-view-item-${ user }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user }`))).toBeVisible();
});
it('should mute user', async() => {
await element(by.id(`room-members-view-item-${ user }`)).longPress();
await waitFor(element(by.text('Mute'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Mute'))).toBeVisible();
await element(by.text('Mute')).tap();
await waitFor(element(by.text('User has been muted!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('User has been muted!'))).toBeVisible();
await waitFor(element(by.text('User has been muted!'))).toBeNotVisible().withTimeout(60000);
await expect(element(by.text('User has been muted!'))).toBeNotVisible();
await element(by.id(`room-members-view-item-${ user }`)).longPress();
await waitFor(element(by.text('Unmute'))).toBeVisible().withTimeout(2000);
await expect(element(by.text('Unmute'))).toBeVisible();
await element(by.text('Unmute')).tap();
await waitFor(element(by.text('User has been unmuted!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('User has been unmuted!'))).toBeVisible();
await waitFor(element(by.text('User has been unmuted!'))).toBeNotVisible().withTimeout(5000);
await expect(element(by.text('User has been unmuted!'))).toBeNotVisible();
});
it('should navigate to direct room', async() => {
await waitFor(element(by.id(`room-members-view-item-${ user }`))).toExist().withTimeout(5000);
await element(by.id(`room-members-view-item-${ user }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('room-view'))).toBeVisible();
await waitFor(element(by.id('room-view-title'))).toHaveText(user).withTimeout(60000);
await expect(element(by.id('room-view-title'))).toHaveText(user);
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('room-list-view'))).toBeVisible().withTimeout(2000);
});
afterEach(async() => {
takeScreenshot();
});
});
})
describe('Direct', async() => {
before(async() => {
await navigateToRoomActions('d');
});
it('should block/unblock user', async() => {
await waitFor(element(by.id('room-actions-block-user'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(scrollDown, 'down');
await element(by.id('room-actions-block-user')).tap();
await waitFor(element(by.text('Unblock user'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Unblock user'))).toBeVisible();
await element(by.id('room-actions-block-user')).tap();
await waitFor(element(by.text('Block user'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Block user'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
});
});

326
e2e/09-roominfo.spec.js Normal file
View File

@ -0,0 +1,326 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
async function navigateToRoomInfo() {
const room = `private${ data.random }`;
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeVisible().withTimeout(2000);
await element(by.id(`rooms-list-view-item-${ room }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
await element(by.id('room-view-header-title')).tap();
await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
}
async function backToRoomsList() {
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(2000);
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
}
describe('Room info screen', () => {
describe('Direct', async() => {
before(async() => {
// last test positioned simulator at rooms-list-actions on a direct room
await waitFor(element(by.id('room-actions-info'))).toBeVisible().whileElement(by.id('room-actions-list')).scroll(500, 'up');
await element(by.id('room-actions-info')).tap();
await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
});
it('should navigate to room info', async() => {
await expect(element(by.id('room-info-view'))).toBeVisible();
await expect(element(by.id('room-info-view-name'))).toBeVisible();
});
after(async() => {
await takeScreenshot();
await backToRoomsList();
});
});
describe('Channel/Group', async() => {
before(async() => {
await navigateToRoomInfo();
});
describe('Render', async() => {
it('should have room info view', async() => {
await expect(element(by.id('room-info-view'))).toBeVisible();
});
it('should have name', async() => {
await expect(element(by.id('room-info-view-name'))).toBeVisible();
});
it('should have description', async() => {
await expect(element(by.id('room-info-view-description'))).toBeVisible();
});
it('should have topic', async() => {
await expect(element(by.id('room-info-view-topic'))).toBeVisible();
});
it('should have announcement', async() => {
await expect(element(by.id('room-info-view-announcement'))).toBeVisible();
});
it('should have edit button', async() => {
await expect(element(by.id('room-info-view-edit-button'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Render Edit', async() => {
before(async() => {
await waitFor(element(by.id('room-info-view-edit-button'))).toBeVisible().withTimeout(10000);
await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toBeVisible().withTimeout(2000);
});
it('should have room info edit view', async() => {
await expect(element(by.id('room-info-edit-view'))).toExist();
});
it('should have name input', async() => {
await expect(element(by.id('room-info-edit-view-name'))).toBeVisible();
});
it('should have description input', async() => {
await expect(element(by.id('room-info-edit-view-description'))).toBeVisible();
});
it('should have topic input', async() => {
await expect(element(by.id('room-info-edit-view-topic'))).toBeVisible();
});
it('should have announcement input', async() => {
await expect(element(by.id('room-info-edit-view-announcement'))).toBeVisible();
});
it('should have password input', async() => {
await expect(element(by.id('room-info-edit-view-password'))).toBeVisible();
});
it('should have type switch', async() => {
await element(by.id('room-info-edit-view-list')).swipe('up');
await expect(element(by.id('room-info-edit-view-t'))).toBeVisible();
});
it('should have ready only switch', async() => {
await expect(element(by.id('room-info-edit-view-ro'))).toBeVisible();
});
it('should have submit button', async() => {
await expect(element(by.id('room-info-edit-view-submit'))).toBeVisible();
});
it('should have reset button', async() => {
await expect(element(by.id('room-info-edit-view-reset'))).toBeVisible();
});
it('should have archive button', async() => {
await expect(element(by.id('room-info-edit-view-archive'))).toBeVisible();
});
it('should have delete button', async() => {
await expect(element(by.id('room-info-edit-view-delete'))).toBeVisible();
});
after(async() => {
await takeScreenshot();
await element(by.id('room-info-edit-view-list')).swipe('down');
});
});
describe('Usage', async() => {
const room = `private${ data.random }`;
// it('should enter "invalid name" and get error', async() => {
// await element(by.id('room-info-edit-view-list')).swipe('down');
// await element(by.id('room-info-edit-view-name')).replaceText('invalid name');
// await element(by.id('room-info-edit-view-list')).swipe('up');
// await element(by.id('room-info-edit-view-submit')).tap();
// await waitFor(element(by.text('There was an error while saving settings!'))).toBeVisible().withTimeout(60000);
// await expect(element(by.text('There was an error while saving settings!'))).toBeVisible();
// await element(by.text('OK')).tap();
// await waitFor(element(by.text('There was an error while saving settings!'))).toBeNotVisible().withTimeout(10000);
// await element(by.id('room-info-edit-view-list')).swipe('down');
// });
it('should change room name', async() => {
await element(by.id('room-info-edit-view-name')).replaceText(`${ room }new`);
await element(by.id('room-info-edit-view-list')).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('room-info-view-name'))).toHaveText(`${ room }new`).withTimeout(60000);
await expect(element(by.id('room-info-view-name'))).toHaveText(`${ room }new`);
// change name to original
await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toBeVisible().withTimeout(2000);
await element(by.id('room-info-edit-view-name')).replaceText(`${ room }`);
await element(by.id('room-info-edit-view-list')).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
await element(by.id('room-info-edit-view-list')).swipe('down');
});
it('should reset form', async() => {
await element(by.id('room-info-edit-view-name')).replaceText('abc');
await element(by.id('room-info-edit-view-description')).replaceText('abc');
await element(by.id('room-info-edit-view-topic')).replaceText('abc');
await element(by.id('room-info-edit-view-announcement')).replaceText('abc');
await element(by.id('room-info-edit-view-password')).replaceText('abc');
await element(by.id('room-info-edit-view-list')).swipe('up');
await element(by.id('room-info-edit-view-t')).tap();
await element(by.id('room-info-edit-view-ro')).tap();
await element(by.id('room-info-edit-view-react-when-ro')).tap();
await element(by.id('room-info-edit-view-reset')).tap();
// after reset
await expect(element(by.id('room-info-edit-view-name'))).toHaveText(room);
await expect(element(by.id('room-info-edit-view-description'))).toHaveText('');
await expect(element(by.id('room-info-edit-view-topic'))).toHaveText('');
await expect(element(by.id('room-info-edit-view-announcement'))).toHaveText('');
await expect(element(by.id('room-info-edit-view-password'))).toHaveText('');
await expect(element(by.id('room-info-edit-view-t'))).toHaveValue('1');
await expect(element(by.id('room-info-edit-view-ro'))).toHaveValue('0');
await expect(element(by.id('room-info-edit-view-react-when-ro'))).toBeNotVisible();
await element(by.id('room-info-edit-view-list')).swipe('down');
});
it('should change room description', async() => {
await element(by.id('room-info-edit-view-description')).replaceText('new description');
await element(by.id('room-info-edit-view-list')).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('room-info-view-description'))).toHaveText('new description').withTimeout(60000);
await expect(element(by.id('room-info-view-description'))).toHaveText('new description');
await waitFor(element(by.id('room-info-view-edit-button'))).toBeVisible().withTimeout(10000);
await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toBeVisible().withTimeout(2000);
});
it('should change room topic', async() => {
await element(by.id('room-info-edit-view-topic')).replaceText('new topic');
await element(by.id('room-info-edit-view-list')).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('room-info-view-topic'))).toHaveText('new topic').withTimeout(60000);
await expect(element(by.id('room-info-view-topic'))).toHaveText('new topic');
await waitFor(element(by.id('room-info-view-edit-button'))).toBeVisible().withTimeout(10000);
await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toBeVisible().withTimeout(2000);
});
it('should change room announcement', async() => {
await element(by.id('room-info-edit-view-announcement')).replaceText('new announcement');
await element(by.id('room-info-edit-view-list')).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
await element(by.id('header-back')).atIndex(0).tap();
await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('room-info-view-announcement'))).toHaveText('new announcement').withTimeout(60000);
await expect(element(by.id('room-info-view-announcement'))).toHaveText('new announcement');
await waitFor(element(by.id('room-info-view-edit-button'))).toBeVisible().withTimeout(10000);
await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toBeVisible().withTimeout(2000);
});
it('should change room password', async() => {
await element(by.id('room-info-edit-view-list')).swipe('up');
await element(by.id('room-info-edit-view-password')).replaceText('password');
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
});
it('should change room type', async() => {
await element(by.id('room-info-edit-view-t')).tap();
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
await element(by.id('room-info-edit-view-t')).tap();
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
});
it('should change room read only and allow reactions', async() => {
await element(by.id('room-info-edit-view-ro')).tap();
await waitFor(element(by.id('room-info-edit-view-react-when-ro'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('room-info-edit-view-react-when-ro'))).toBeVisible();
await element(by.id('room-info-edit-view-react-when-ro')).tap();
await element(by.id('room-info-edit-view-submit')).tap();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Settings succesfully changed!'))).toBeVisible();
await waitFor(element(by.text('Settings succesfully changed!'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.text('Settings succesfully changed!'))).toBeNotVisible();
// TODO: test if it's possible to react
});
it('should archive room', async() => {
await element(by.id('room-info-edit-view-archive')).tap();
await waitFor(element(by.text('Yes, archive it!'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Yes, archive it!'))).toBeVisible();
await element(by.text('Yes, archive it!')).tap();
await waitFor(element(by.text('UNARCHIVE'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('UNARCHIVE'))).toBeVisible();
// TODO: needs permission to unarchive
// await element(by.id('room-info-edit-view-archive')).tap();
// await waitFor(element(by.text('Yes, unarchive it!'))).toBeVisible().withTimeout(5000);
// await expect(element(by.text('Yes, unarchive it!'))).toBeVisible();
// await element(by.text('Yes, unarchive it!')).tap();
// await waitFor(element(by.text('ARCHIVE'))).toBeVisible().withTimeout(60000);
// await expect(element(by.text('ARCHIVE'))).toBeVisible();
});
it('should delete room', async() => {
await element(by.id('room-info-edit-view-list')).swipe('up');
await element(by.id('room-info-edit-view-delete')).tap();
await waitFor(element(by.text('Yes, delete it!'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Yes, delete it!'))).toBeVisible();
await element(by.text('Yes, delete it!')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible();
});
after(async() => {
takeScreenshot();
});
});
});
});

View File

@ -0,0 +1,60 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
describe('Change server', () => {
before(async() => {
await device.reloadReactNative();
});
it('should add server and create new user', async() => {
// Navigate to add server
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
await element(by.id('sidebar-add-server')).tap();
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
// Add server
await element(by.id('new-server-view-input')).replaceText(data.alternateServer);
await waitFor(element(by.text(' is a valid Rocket.Chat instance'))).toBeVisible().withTimeout(60000);
await element(by.id('new-server-view-button')).tap();
// Navigate to register
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000);
await element(by.id('welcome-view-register')).tap();
await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000);
// Register new user
await element(by.id('register-view-name')).replaceText(data.user);
await element(by.id('register-view-email')).replaceText(data.email);
await element(by.id('register-view-password')).replaceText(data.password);
await element(by.id('register-view-repeat-password')).replaceText(data.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('register-view-username'))).toBeVisible().withTimeout(60000);
await element(by.id('register-view-username')).replaceText(data.user);
await element(by.id('register-view-submit-username')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await expect(element(by.id('rooms-list-view-sidebar'))).toHaveLabel(`Connected to ${ data.alternateServer }. Tap to view servers list.`);
// For a sanity test, to make sure roomslist is showing correct rooms
// app CANNOT show public room created on previous tests
await waitFor(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeNotVisible();
});
it('should change server', async() => {
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
await element(by.id(`sidebar-${ data.server }`)).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
await waitFor(element(by.id('rooms-list-view-sidebar').and(by.label(`Connected to ${ data.server }. Tap to view servers list.`)))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view-sidebar'))).toHaveLabel(`Connected to ${ data.server }. Tap to view servers list.`);
// For a sanity test, to make sure roomslist is showing correct rooms
// app MUST show public room created on previous tests
await waitFor(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});

11
e2e/data.js Normal file
View File

@ -0,0 +1,11 @@
const random = require('./helpers/random');
const value = random(20);
const data = {
server: 'https://unstable.rocket.chat',
alternateServer: 'https://stable.rocket.chat',
user: `user${ value }`,
password: `password${ value }`,
email: `detoxrn+${ value }@rocket.chat`,
random: value
}
module.exports = data;

41
e2e/helpers/app.js Normal file
View File

@ -0,0 +1,41 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const data = require('../data');
async function addServer() {
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
await element(by.id('new-server-view-input')).replaceText(data.server);
await waitFor(element(by.text(' is a valid Rocket.Chat instance'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('new-server-view-button'))).toBeVisible().withTimeout(2000);
await element(by.id('new-server-view-button')).tap();
}
async function navigateToLogin() {
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000);
await element(by.id('welcome-view-login')).tap();
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000);
}
async function login() {
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000);
await element(by.id('login-view-email')).replaceText(data.user);
await element(by.id('login-view-password')).replaceText(data.password);
await element(by.id('login-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
}
async function logout() {
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
await element(by.id('sidebar-logout')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('welcome-view'))).toBeVisible();
}
module.exports = {
addServer,
navigateToLogin,
login,
logout
};

9
e2e/helpers/random.js Normal file
View File

@ -0,0 +1,9 @@
function random(length) {
let text = '';
const possible = 'abcdefghijklmnopqrstuvwxyz';
for (let i = 0; i < length; i += 1) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
module.exports = random;

23
e2e/helpers/screenshot.js Normal file
View File

@ -0,0 +1,23 @@
const { execSync } = require('child_process');
const { existsSync, mkdirSync } = require('fs');
const SCREENSHOT_DIR = '/tmp/screenshots';
const SCREENSHOT_OPTIONS = {
timeout: 2000,
killSignal: 'SIGKILL'
};
let screenshotIndex = 0;
const takeScreenshot = () => {
if (!existsSync(SCREENSHOT_DIR)) { mkdirSync(SCREENSHOT_DIR); }
const screenshotFilename = `${ SCREENSHOT_DIR }/screenshot-${ screenshotIndex++ }.png`;
try {
execSync(`xcrun simctl io booted screenshot ${ screenshotFilename }`, SCREENSHOT_OPTIONS);
} catch (error) {
console.log('erro');
}
};
module.exports = { takeScreenshot };

11
e2e/init.js Normal file
View File

@ -0,0 +1,11 @@
const detox = require('detox');
const config = require('../package.json').detox;
before(async() => {
await detox.init(config);
await device.launchApp({ permissions: { notifications: 'YES' } });
});
after(async() => {
await detox.cleanup();
});

1
e2e/mocha.opts Normal file
View File

@ -0,0 +1 @@
--recursive --timeout 120000

178
package-lock.json generated
View File

@ -3619,6 +3619,12 @@
} }
} }
}, },
"browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
},
"browserify-aes": { "browserify-aes": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@ -3941,6 +3947,29 @@
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
}, },
"child-process-promise": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz",
"integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=",
"dev": true,
"requires": {
"cross-spawn": "4.0.2",
"node-version": "1.1.3",
"promise-polyfill": "6.1.0"
},
"dependencies": {
"cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
"integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=",
"dev": true,
"requires": {
"lru-cache": "4.1.1",
"which": "1.3.0"
}
}
}
},
"chokidar": { "chokidar": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz",
@ -5534,6 +5563,59 @@
"defined": "1.0.0" "defined": "1.0.0"
} }
}, },
"detox": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/detox/-/detox-7.3.5.tgz",
"integrity": "sha512-qFAlpFAR7KOZLFoVyGHUfvvfeC6ULzdFCMnM/qlCt2rRkCC4hB1KEtzsPng92N+Si+ZNeByJ+mjWt68nbamsUQ==",
"dev": true,
"requires": {
"child-process-promise": "2.2.1",
"commander": "2.15.1",
"detox-server": "7.0.0",
"fs-extra": "4.0.3",
"get-port": "2.1.0",
"ini": "1.3.5",
"lodash": "4.17.10",
"npmlog": "4.1.2",
"shell-utils": "1.0.9",
"tail": "1.2.3",
"telnet-client": "0.15.3",
"ws": "1.1.5"
},
"dependencies": {
"fs-extra": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz",
"integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
"dev": true,
"requires": {
"graceful-fs": "4.1.11",
"jsonfile": "4.0.0",
"universalify": "0.1.1"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true,
"requires": {
"graceful-fs": "4.1.11"
}
}
}
},
"detox-server": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/detox-server/-/detox-server-7.0.0.tgz",
"integrity": "sha512-zs9ZP/MgeEmaZD/+MCl5PVcYHRjUtFBkBx3xQRPcsjJ/PmpCKy/BvygjLO6tRsR/2SC9UYay6W+BdguEYeft8g==",
"dev": true,
"requires": {
"lodash": "4.17.10",
"npmlog": "4.1.2",
"ws": "1.1.5"
}
},
"diff": { "diff": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
@ -7901,6 +7983,15 @@
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U="
}, },
"get-port": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-2.1.0.tgz",
"integrity": "sha1-h4P53OvR7qSVozThpqJR54iHqxo=",
"dev": true,
"requires": {
"pinkie-promise": "2.0.1"
}
},
"get-stdin": { "get-stdin": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz",
@ -8063,6 +8154,12 @@
"lodash": "4.17.10" "lodash": "4.17.10"
} }
}, },
"growl": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
"integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
"dev": true
},
"growly": { "growly": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
@ -11452,6 +11549,51 @@
"resolved": "https://registry.npmjs.org/mobx/-/mobx-2.7.0.tgz", "resolved": "https://registry.npmjs.org/mobx/-/mobx-2.7.0.tgz",
"integrity": "sha1-zz2C0YwMp/RY2PKiQIF7PcflSgE=" "integrity": "sha1-zz2C0YwMp/RY2PKiQIF7PcflSgE="
}, },
"mocha": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.1.1.tgz",
"integrity": "sha512-kKKs/H1KrMMQIEsWNxGmb4/BGsmj0dkeyotEvbrAuQ01FcWRLssUNXCEUZk6SZtyJBi6EE7SL0zDDtItw1rGhw==",
"dev": true,
"requires": {
"browser-stdout": "1.3.1",
"commander": "2.11.0",
"debug": "3.1.0",
"diff": "3.5.0",
"escape-string-regexp": "1.0.5",
"glob": "7.1.2",
"growl": "1.10.3",
"he": "1.1.1",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"supports-color": "4.4.0"
},
"dependencies": {
"commander": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
"dev": true
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"supports-color": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
"integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
"dev": true,
"requires": {
"has-flag": "2.0.0"
}
}
}
},
"moment": { "moment": {
"version": "2.22.1", "version": "2.22.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz",
@ -11890,6 +12032,12 @@
} }
} }
}, },
"node-version": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/node-version/-/node-version-1.1.3.tgz",
"integrity": "sha512-rEwE51JWn0yN3Wl5BXeGn5d52OGbSXzWiiXRjAQeuyvcGKyvuSILW2rb3G7Xh+nexzLwhTpek6Ehxd6IjvHePg==",
"dev": true
},
"nopt": { "nopt": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
@ -13769,6 +13917,12 @@
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM="
}, },
"promise-polyfill": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz",
"integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=",
"dev": true
},
"prop-types": { "prop-types": {
"version": "15.6.1", "version": "15.6.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz",
@ -16137,6 +16291,15 @@
"jsonify": "0.0.0" "jsonify": "0.0.0"
} }
}, },
"shell-utils": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/shell-utils/-/shell-utils-1.0.9.tgz",
"integrity": "sha512-JbTHnKpMyj9TUUbL+Us2Rx2iVHFvH5QyQoke9SN1L0pueiZeO2Gzlzopmloi7oqObL4qtvdSuZPE3UfdIzmlag==",
"dev": true,
"requires": {
"lodash": "4.17.10"
}
},
"shelljs": { "shelljs": {
"version": "0.8.2", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.2.tgz", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.2.tgz",
@ -17243,6 +17406,12 @@
} }
} }
}, },
"tail": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/tail/-/tail-1.2.3.tgz",
"integrity": "sha1-sI1vp5+5KIaWMaNBpRwUSXwcQlU=",
"dev": true
},
"tapable": { "tapable": {
"version": "0.2.8", "version": "0.2.8",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz",
@ -17287,6 +17456,15 @@
"xtend": "4.0.1" "xtend": "4.0.1"
} }
}, },
"telnet-client": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-0.15.3.tgz",
"integrity": "sha512-GSfdzQV0BKIYsmeXq7bJFJ2wHeJud6icaIxCUf6QCGQUD6R0BBGbT1+yLDhq67JRdgRpwyPwUbV7JxFeRrZomQ==",
"dev": true,
"requires": {
"bluebird": "3.5.1"
}
},
"temp": { "temp": {
"version": "0.8.3", "version": "0.8.3",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz",

View File

@ -87,6 +87,7 @@
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"babel-preset-react-native": "^4.0.0", "babel-preset-react-native": "^4.0.0",
"codecov": "^3.0.2", "codecov": "^3.0.2",
"detox": "^7.3.5",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-config-airbnb": "^16.1.0", "eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.12.0", "eslint-plugin-import": "^2.12.0",
@ -96,6 +97,7 @@
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^22.4.4", "jest": "^22.4.4",
"jest-cli": "^22.4.4", "jest-cli": "^22.4.4",
"mocha": "^5.1.1",
"react-dom": "^16.3.2", "react-dom": "^16.3.2",
"react-native-bundle-visualizer": "^1.2.0", "react-native-bundle-visualizer": "^1.2.0",
"react-test-renderer": "^16.3.2", "react-test-renderer": "^16.3.2",
@ -104,6 +106,9 @@
"reactotron-redux-saga": "^1.13.0" "reactotron-redux-saga": "^1.13.0"
}, },
"jest": { "jest": {
"testPathIgnorePatterns": [
"e2e"
],
"preset": "react-native", "preset": "react-native",
"coverageDirectory": "./coverage/", "coverageDirectory": "./coverage/",
"collectCoverage": true, "collectCoverage": true,
@ -115,5 +120,15 @@
"engines": { "engines": {
"node": ">=8.x", "node": ">=8.x",
"npm": ">=4.x" "npm": ">=4.x"
},
"detox": {
"configurations": {
"ios.sim.debug": {
"binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/RocketChatRN.app",
"build": "xcodebuild -project ios/RocketChatRN.xcodeproj -scheme RocketChatRN -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
"type": "ios.simulator",
"name": "iPhone 7"
}
}
} }
} }