Merge branch 'develop' into chore/ts-RoomView

This commit is contained in:
AlexAlexandre 2021-12-02 14:50:55 -03:00
commit b1d1e29d41
69 changed files with 1345 additions and 811 deletions

View File

@ -1419,6 +1419,7 @@ Array [
</Text>
</View>
<View
accessibilityLabel="Use"
accessible={true}
focusable={true}
onClick={[Function]}
@ -1581,6 +1582,7 @@ Array [
</Text>
</View>
<View
accessibilityLabel="Use"
accessible={true}
focusable={true}
onClick={[Function]}
@ -41244,6 +41246,7 @@ exports[`Storyshots Message Show a button as attachment 1`] = `
Test Button
</Text>
<View
accessibilityLabel="Text button"
accessible={true}
focusable={true}
onClick={[Function]}

View File

@ -151,6 +151,8 @@ android {
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
}
resValue "string", "rn_config_reader_custom_package", "chat.rocket.reactnative"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
signingConfigs {
@ -203,6 +205,10 @@ android {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
e2e {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
foss {
dimension = "type"
buildConfigField "boolean", "FDROID_BUILD", "true"
@ -230,6 +236,16 @@ android {
java.srcDirs = ['src/main/java', 'src/play/java']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
e2ePlayDebug {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
e2ePlayRelease {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
}
applicationVariants.all { variant ->
@ -294,6 +310,8 @@ dependencies {
implementation "com.tencent:mmkv-static:1.2.1"
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.9.0"
androidTestImplementation('com.wix:detox:+') { transitive = true }
androidTestImplementation 'junit:junit:4.12'
}
// Run this once to be able to run the application with BUCK

View File

@ -0,0 +1,32 @@
// Replace "com.example" here and below with your app's package name from the top of MainActivity.java
package chat.rocket.reactnative;
import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class DetoxTest {
@Rule
// Replace 'MainActivity' with the value of android:name entry in
// <activity> in AndroidManifest.xml
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
@Test
public void runDetoxTests() {
DetoxConfig detoxConfig = new DetoxConfig();
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
detoxConfig.rnContextLoadTimeoutSec = (chat.rocket.reactnative.BuildConfig.DEBUG ? 180 : 60);
Detox.runTests(mActivityRule, detoxConfig);
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@ -53,6 +53,10 @@ allprojects {
url("$rootDir/../node_modules/jsc-android/dist")
}
maven {
url "$rootDir/../node_modules/detox/Detox-android"
}
maven {
url jitsi_url
}

View File

@ -124,7 +124,11 @@ const ActionSheet = React.memo(
const renderFooter = () =>
data?.hasCancel ? (
<Button onPress={hide} style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]} theme={theme}>
<Button
onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
accessibilityLabel={I18n.t('Cancel')}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text>
</Button>
) : null;

View File

@ -70,6 +70,7 @@ export default class Button extends React.PureComponent<Partial<IButtonProps>, a
disabled && styles.disabled,
style
]}
accessibilityLabel={title}
{...otherProps}>
{loading ? (
<ActivityIndicator color={textColor} />

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, View } from 'react-native';
interface IHeaderButtonContainer {
children: JSX.Element;
children: React.ReactNode;
left?: boolean;
}

View File

@ -20,7 +20,7 @@ interface IPasscodeBase {
previousPasscode?: string;
title: string;
subtitle?: string;
showBiometry?: string;
showBiometry?: boolean;
onEndProcess: Function;
onError?: Function;
onBiometryPress?(): void;

View File

@ -15,7 +15,7 @@ import I18n from '../../i18n';
interface IPasscodePasscodeEnter {
theme: string;
hasBiometry: string;
hasBiometry: boolean;
finishProcess: Function;
}

View File

@ -8,6 +8,7 @@ interface IStatus {
status: string;
size: number;
style?: StyleProp<TextStyle>;
testID?: string;
}
const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => {

View File

@ -43,7 +43,11 @@ const Content = React.memo(
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else if (props.isEncrypted) {
content = (
<Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{I18n.t('Encrypted_message')}</Text>
<Text
style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}
accessibilityLabel={I18n.t('Encrypted_message')}>
{I18n.t('Encrypted_message')}
</Text>
);
} else {
const { baseUrl, user, onLinkPress } = useContext(MessageContext);

View File

@ -1,5 +1,5 @@
// https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts
export const TEAM_TYPE = {
exports.TEAM_TYPE = {
PUBLIC: 0,
PRIVATE: 1
};

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Text, View } from 'react-native';
import { Text, View, ViewStyle } from 'react-native';
import Touch from '../../utils/touch';
import Avatar from '../../containers/Avatar';
@ -10,7 +10,7 @@ import { themes } from '../../constants/colors';
export { ROW_HEIGHT };
interface IDirectoryItemLabel {
text: string;
text?: string;
theme: string;
}
@ -21,9 +21,9 @@ interface IDirectoryItem {
type: string;
onPress(): void;
testID: string;
style: any;
rightLabel: string;
rid: string;
style?: ViewStyle;
rightLabel?: string;
rid?: string;
theme: string;
teamMain?: boolean;
}
@ -32,7 +32,7 @@ const DirectoryItemLabel = React.memo(({ text, theme }: IDirectoryItemLabel) =>
if (!text) {
return null;
}
return <Text style={[styles.directoryItemLabel, { color: themes[theme!].auxiliaryText }]}>{text}</Text>;
return <Text style={[styles.directoryItemLabel, { color: themes[theme].auxiliaryText }]}>{text}</Text>;
});
const DirectoryItem = ({

View File

@ -4,7 +4,7 @@ import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/r
import scrollPersistTaps from '../utils/scrollPersistTaps';
interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
keyboardVerticalOffset: number;
keyboardVerticalOffset?: number;
scrollEnabled?: boolean;
children: React.ReactNode;
}

View File

@ -46,7 +46,7 @@ interface IUserItem {
testID: string;
onLongPress?: () => void;
style?: StyleProp<ViewStyle>;
icon: string;
icon?: string;
theme: string;
}

View File

@ -63,7 +63,11 @@ export default class DirectoryOptions extends PureComponent<IDirectoryOptionsPro
}
return (
<Touch onPress={() => changeType(itemType)} style={styles.dropdownItemButton} theme={theme}>
<Touch
onPress={() => changeType(itemType)}
style={styles.dropdownItemButton}
theme={theme}
accessibilityLabel={I18n.t(text)}>
<View style={styles.dropdownItemContainer}>
<CustomIcon style={[styles.dropdownItemIcon, { color: themes[theme].bodyText }]} size={22} name={icon} />
<Text style={[styles.dropdownItemText, { color: themes[theme].bodyText }]}>{I18n.t(text)}</Text>
@ -90,7 +94,7 @@ export default class DirectoryOptions extends PureComponent<IDirectoryOptionsPro
</TouchableWithoutFeedback>
<Animated.View
style={[styles.dropdownContainer, { transform: [{ translateY }], backgroundColor: themes[theme].backgroundColor }]}>
<Touch onPress={this.close} theme={theme}>
<Touch onPress={this.close} theme={theme} accessibilityLabel={I18n.t('Search_by')}>
<View
style={[
styles.dropdownContainerHeader,

View File

@ -1,11 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import { dequal } from 'dequal';
import * as List from '../containers/List';
import * as List from '../containers/List';
import Touch from '../utils/touch';
import database from '../lib/database';
import RocketChat from '../lib/rocketchat';
@ -18,7 +19,6 @@ import * as HeaderButton from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
import { themes } from '../constants/colors';
import { withTheme } from '../theme';
import { getUserSelector } from '../selectors/login';
import Navigation from '../lib/Navigation';
import { createChannelRequest } from '../actions/createChannel';
import { goRoom } from '../utils/goRoom';
@ -47,33 +47,54 @@ const styles = StyleSheet.create({
}
});
class NewMessageView extends React.Component {
static navigationOptions = ({ navigation }) => ({
interface IButton {
onPress: () => void;
testID: string;
title: string;
icon: string;
first?: boolean;
}
interface ISearch {
_id: string;
status: string;
username: string;
avatarETag: string;
outside: boolean;
rid: string;
name: string;
t: string;
search: boolean;
}
interface INewMessageViewState {
search: ISearch[];
// TODO: Refactor when migrate room
chats: any[];
permissions: boolean[];
}
interface INewMessageViewProps {
navigation: StackNavigationProp<any, 'NewMessageView'>;
create: (params: { group: boolean }) => void;
maxUsers: number;
theme: string;
isMasterDetail: boolean;
serverVersion: string;
createTeamPermission: string[];
createDirectMessagePermission: string[];
createPublicChannelPermission: string[];
createPrivateChannelPermission: string[];
createDiscussionPermission: string[];
}
class NewMessageView extends React.Component<INewMessageViewProps, INewMessageViewState> {
static navigationOptions = ({ navigation }: INewMessageViewProps): StackNavigationOptions => ({
headerLeft: () => <HeaderButton.CloseModal navigation={navigation} testID='new-message-view-close' />,
title: I18n.t('New_Message')
});
static propTypes = {
navigation: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string,
roles: PropTypes.array
}),
create: PropTypes.func,
maxUsers: PropTypes.number,
theme: PropTypes.string,
isMasterDetail: PropTypes.bool,
serverVersion: PropTypes.string,
createTeamPermission: PropTypes.array,
createDirectMessagePermission: PropTypes.array,
createPublicChannelPermission: PropTypes.array,
createPrivateChannelPermission: PropTypes.array,
createDiscussionPermission: PropTypes.array
};
constructor(props) {
constructor(props: INewMessageViewProps) {
super(props);
this.init();
this.state = {
@ -102,7 +123,7 @@ class NewMessageView extends React.Component {
this.handleHasPermission();
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: INewMessageViewProps) {
const {
createTeamPermission,
createPublicChannelPermission,
@ -122,7 +143,7 @@ class NewMessageView extends React.Component {
}
}
onSearchChangeText(text) {
onSearchChangeText(text: string) {
this.search(text);
}
@ -131,7 +152,7 @@ class NewMessageView extends React.Component {
return navigation.pop();
};
search = async text => {
search = async (text: string) => {
const result = await RocketChat.search({ text, filterRooms: false });
this.setState({
search: result
@ -162,7 +183,8 @@ class NewMessageView extends React.Component {
});
};
goRoom = item => {
// TODO: Refactor when migrate room
goRoom = (item: any) => {
logEvent(events.NEW_MSG_CHAT_WITH_USER);
const { isMasterDetail, navigation } = this.props;
if (isMasterDetail) {
@ -171,7 +193,7 @@ class NewMessageView extends React.Component {
goRoom({ item, isMasterDetail });
};
renderButton = ({ onPress, testID, title, icon, first }) => {
renderButton = ({ onPress, testID, title, icon, first }: IButton) => {
const { theme } = this.props;
return (
<Touch onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }} testID={testID} theme={theme}>
@ -218,7 +240,7 @@ class NewMessageView extends React.Component {
return (
<View style={{ backgroundColor: themes[theme].auxiliaryBackground }}>
<SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='new-message-view-search' />
<SearchBox onChangeText={(text: string) => this.onSearchChangeText(text)} testID='new-message-view-search' />
<View style={styles.buttonContainer}>
{permissions[0] || permissions[1]
? this.renderButton({
@ -258,9 +280,10 @@ class NewMessageView extends React.Component {
);
};
renderItem = ({ item, index }) => {
// TODO: Refactor when migrate room
renderItem = ({ item, index }: { item: ISearch | any; index: number }) => {
const { search, chats } = this.state;
const { baseUrl, user, theme } = this.props;
const { theme } = this.props;
let style = { borderColor: themes[theme].separatorColor };
if (index === 0) {
@ -277,10 +300,8 @@ class NewMessageView extends React.Component {
name={item.search ? item.name : item.fname}
username={item.search ? item.username : item.name}
onPress={() => this.goRoom(item)}
baseUrl={baseUrl}
testID={`new-message-view-item-${item.name}`}
style={style}
user={user}
theme={theme}
/>
);
@ -313,12 +334,10 @@ class NewMessageView extends React.Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
serverVersion: state.server.version,
isMasterDetail: state.app.isMasterDetail,
baseUrl: state.server.server,
maxUsers: state.settings.DirectMesssage_maxUsers || 1,
user: getUserSelector(state),
createTeamPermission: state.permissions['create-team'],
createDirectMessagePermission: state.permissions['create-d'],
createPublicChannelPermission: state.permissions['create-c'],
@ -326,8 +345,8 @@ const mapStateToProps = state => ({
createDiscussionPermission: state.permissions['start-discussion']
});
const mapDispatchToProps = dispatch => ({
create: params => dispatch(createChannelRequest(params))
const mapDispatchToProps = (dispatch: Dispatch) => ({
create: (params: { group: boolean }) => dispatch(createChannelRequest(params))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView));

View File

@ -918,7 +918,6 @@ class RoomActionsView extends React.Component {
event: this.convertTeamToChannel
})
}
testID='room-actions-convert-channel-to-team'
left={() => <List.Icon name='channel-public' />}
showActionIndicator
/>

View File

@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Switch } from 'react-native';
import { connect } from 'react-redux';
import { StackNavigationOptions } from '@react-navigation/stack';
import Model from '@nozbe/watermelondb/Model';
import { Subscription } from 'rxjs';
import I18n from '../i18n';
import { withTheme } from '../theme';
@ -16,19 +18,42 @@ import { events, logEvent } from '../utils/log';
const DEFAULT_BIOMETRY = false;
class ScreenLockConfigView extends React.Component {
static navigationOptions = () => ({
interface IServerRecords extends Model {
autoLock?: boolean;
autoLockTime?: number;
biometry?: boolean;
}
interface IItem {
title: string;
value: number;
disabled?: boolean;
}
interface IScreenLockConfigViewProps {
theme: string;
server: string;
Force_Screen_Lock: boolean;
Force_Screen_Lock_After: number;
}
interface IScreenLockConfigViewState {
autoLock?: boolean;
autoLockTime?: number | null;
biometry?: boolean;
biometryLabel: null;
}
class ScreenLockConfigView extends React.Component<IScreenLockConfigViewProps, IScreenLockConfigViewState> {
private serverRecord?: IServerRecords;
private observable?: Subscription;
static navigationOptions = (): StackNavigationOptions => ({
title: I18n.t('Screen_lock')
});
static propTypes = {
theme: PropTypes.string,
server: PropTypes.string,
Force_Screen_Lock: PropTypes.string,
Force_Screen_Lock_After: PropTypes.string
};
constructor(props) {
constructor(props: IScreenLockConfigViewProps) {
super(props);
this.state = {
autoLock: false,
@ -104,7 +129,7 @@ class ScreenLockConfigView extends React.Component {
logEvent(events.SLC_SAVE_SCREEN_LOCK);
const { autoLock, autoLockTime, biometry } = this.state;
const serversDB = database.servers;
await serversDB.action(async () => {
await serversDB.write(async () => {
await this.serverRecord?.update(record => {
record.autoLock = autoLock;
record.autoLockTime = autoLockTime === null ? DEFAULT_AUTO_LOCK : autoLockTime;
@ -113,7 +138,7 @@ class ScreenLockConfigView extends React.Component {
});
};
changePasscode = async ({ force }) => {
changePasscode = async ({ force }: { force: boolean }) => {
logEvent(events.SLC_CHANGE_PASSCODE);
await changePasscode({ force });
};
@ -144,12 +169,12 @@ class ScreenLockConfigView extends React.Component {
);
};
isSelected = value => {
isSelected = (value: number) => {
const { autoLockTime } = this.state;
return autoLockTime === value;
};
changeAutoLockTime = autoLockTime => {
changeAutoLockTime = (autoLockTime: number) => {
logEvent(events.SLC_CHANGE_AUTOLOCK_TIME);
this.setState({ autoLockTime }, () => this.save());
};
@ -159,7 +184,7 @@ class ScreenLockConfigView extends React.Component {
return <List.Icon name='check' color={themes[theme].tintColor} />;
};
renderItem = ({ item }) => {
renderItem = ({ item }: { item: IItem }) => {
const { title, value, disabled } = item;
return (
<>
@ -194,7 +219,7 @@ class ScreenLockConfigView extends React.Component {
if (!autoLock) {
return null;
}
let items = this.defaultAutoLockOptions;
let items: IItem[] = this.defaultAutoLockOptions;
if (Force_Screen_Lock && Force_Screen_Lock_After > 0) {
items = [
{
@ -262,7 +287,7 @@ class ScreenLockConfigView extends React.Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
server: state.server.server,
Force_Screen_Lock: state.settings.Force_Screen_Lock,
Force_Screen_Lock_After: state.settings.Force_Screen_Lock_After

View File

@ -1,19 +1,25 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import useDeepCompareEffect from 'use-deep-compare-effect';
import isEmpty from 'lodash/isEmpty';
import Orientation from 'react-native-orientation-locker';
import { withTheme } from '../theme';
import { useTheme } from '../theme';
import EventEmitter from '../utils/events';
import { LOCAL_AUTHENTICATE_EMITTER } from '../constants/localAuthentication';
import { isTablet } from '../utils/deviceInfo';
import { PasscodeEnter } from '../containers/Passcode';
const ScreenLockedView = ({ theme }) => {
interface IData {
submit?: () => void;
hasBiometry?: boolean;
}
const ScreenLockedView = (): JSX.Element => {
const [visible, setVisible] = useState(false);
const [data, setData] = useState({});
const [data, setData] = useState<IData>({});
const { theme } = useTheme();
useDeepCompareEffect(() => {
if (!isEmpty(data)) {
@ -23,7 +29,7 @@ const ScreenLockedView = ({ theme }) => {
}
}, [data]);
const showScreenLock = args => {
const showScreenLock = (args: IData) => {
setData(args);
};
@ -56,13 +62,9 @@ const ScreenLockedView = ({ theme }) => {
style={{ margin: 0 }}
animationIn='fadeIn'
animationOut='fadeOut'>
<PasscodeEnter theme={theme} hasBiometry={data?.hasBiometry} finishProcess={onSubmit} />
<PasscodeEnter theme={theme} hasBiometry={!!data?.hasBiometry} finishProcess={onSubmit} />
</Modal>
);
};
ScreenLockedView.propTypes = {
theme: PropTypes.string
};
export default withTheme(ScreenLockedView);
export default ScreenLockedView;

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Switch } from 'react-native';
import PropTypes from 'prop-types';
import { StackNavigationProp } from '@react-navigation/stack';
import AsyncStorage from '@react-native-community/async-storage';
import { useSelector } from 'react-redux';
@ -20,11 +20,15 @@ import {
import SafeAreaView from '../containers/SafeAreaView';
import { isFDroidBuild } from '../constants/environment';
const SecurityPrivacyView = ({ navigation }) => {
interface ISecurityPrivacyViewProps {
navigation: StackNavigationProp<any, 'SecurityPrivacyView'>;
}
const SecurityPrivacyView = ({ navigation }: ISecurityPrivacyViewProps): JSX.Element => {
const [crashReportState, setCrashReportState] = useState(getReportCrashErrorsValue());
const [analyticsEventsState, setAnalyticsEventsState] = useState(getReportAnalyticsEventsValue());
const e2eEnabled = useSelector(state => state.settings.E2E_Enable);
const e2eEnabled = useSelector((state: any) => state.settings.E2E_Enable);
useEffect(() => {
navigation.setOptions({
@ -32,21 +36,22 @@ const SecurityPrivacyView = ({ navigation }) => {
});
}, []);
const toggleCrashReport = value => {
logEvent(events.SE_TOGGLE_CRASH_REPORT);
const toggleCrashReport = (value: boolean) => {
logEvent(events.SP_TOGGLE_CRASH_REPORT);
AsyncStorage.setItem(CRASH_REPORT_KEY, JSON.stringify(value));
setCrashReportState(value);
toggleCrashErrorsReport(value);
};
const toggleAnalyticsEvents = value => {
logEvent(events.SE_TOGGLE_ANALYTICS_EVENTS);
const toggleAnalyticsEvents = (value: boolean) => {
logEvent(events.SP_TOGGLE_ANALYTICS_EVENTS);
AsyncStorage.setItem(ANALYTICS_EVENTS_KEY, JSON.stringify(value));
setAnalyticsEventsState(value);
toggleAnalyticsEventsReport(value);
};
const navigateToScreen = screen => {
const navigateToScreen = (screen: 'E2EEncryptionSecurityView' | 'ScreenLockConfigView') => {
// @ts-ignore
logEvent(events[`SP_GO_${screen.replace('View', '').toUpperCase()}`]);
navigation.navigate(screen);
};
@ -106,8 +111,4 @@ const SecurityPrivacyView = ({ navigation }) => {
);
};
SecurityPrivacyView.propTypes = {
navigation: PropTypes.object
};
export default SecurityPrivacyView;

View File

@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { connect } from 'react-redux';
import { RadioButton } from 'react-native-ui-lib';
import { RouteProp } from '@react-navigation/native';
import log from '../utils/log';
import * as List from '../containers/List';
@ -25,15 +26,58 @@ const styles = StyleSheet.create({
}
});
class SelectListView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
route: PropTypes.object,
theme: PropTypes.string,
isMasterDetail: PropTypes.bool
};
interface IData {
rid: string;
name: string;
t?: string;
teamMain?: boolean;
alert?: boolean;
}
constructor(props) {
interface ISelectListViewState {
data: IData[];
dataFiltered: IData[];
isSearching: boolean;
selected: string[];
}
interface ISelectListViewProps {
navigation: StackNavigationProp<any, 'SelectListView'>;
route: RouteProp<
{
SelectView: {
data: IData[];
title: string;
infoText: string;
nextAction(selected: string[]): void;
showAlert(): void;
isSearch: boolean;
onSearch(text: string): IData[];
isRadio: boolean;
};
},
'SelectView'
>;
theme: string;
isMasterDetail: boolean;
}
class SelectListView extends React.Component<ISelectListViewProps, ISelectListViewState> {
private title: string;
private infoText: string;
private nextAction: (selected: string[]) => void;
private showAlert: () => void;
private isSearch: boolean;
private onSearch: (text: string) => IData[];
private isRadio: boolean;
constructor(props: ISelectListViewProps) {
super(props);
const data = props.route?.params?.data;
this.title = props.route?.params?.title;
@ -56,7 +100,7 @@ class SelectListView extends React.Component {
const { navigation, isMasterDetail } = this.props;
const { selected } = this.state;
const options = {
const options: StackNavigationOptions = {
headerTitle: I18n.t(this.title)
};
@ -87,7 +131,7 @@ class SelectListView extends React.Component {
return (
<View style={{ backgroundColor: themes[theme].auxiliaryBackground }}>
<SearchBox
onChangeText={text => this.search(text)}
onChangeText={(text: string) => this.search(text)}
testID='select-list-view-search'
onCancelPress={() => this.setState({ isSearching: false })}
/>
@ -95,7 +139,7 @@ class SelectListView extends React.Component {
);
};
search = async text => {
search = async (text: string) => {
try {
this.setState({ isSearching: true });
const result = await this.onSearch(text);
@ -105,12 +149,12 @@ class SelectListView extends React.Component {
}
};
isChecked = rid => {
isChecked = (rid: string) => {
const { selected } = this.state;
return selected.includes(rid);
};
toggleItem = rid => {
toggleItem = (rid: string) => {
const { selected } = this.state;
animateNextTransition();
@ -126,7 +170,7 @@ class SelectListView extends React.Component {
}
};
renderItem = ({ item }) => {
renderItem = ({ item }: { item: IData }) => {
const { theme } = this.props;
const { selected } = this.state;
@ -187,7 +231,7 @@ class SelectListView extends React.Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
isMasterDetail: state.app.isMasterDetail
});

View File

@ -1,8 +1,8 @@
import React from 'react';
import { FlatList } from 'react-native';
import PropTypes from 'prop-types';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import { Q, Model } from '@nozbe/watermelondb';
import I18n from '../i18n';
import StatusBar from '../containers/StatusBar';
@ -12,29 +12,39 @@ import database from '../lib/database';
import SafeAreaView from '../containers/SafeAreaView';
import * as List from '../containers/List';
const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
const keyExtractor = item => item.id;
const getItemLayout = (data: any, index: number) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
const keyExtractor = (item: IServer) => item.id;
class SelectServerView extends React.Component {
static navigationOptions = () => ({
interface IServer extends Model {
id: string;
iconURL?: string;
name?: string;
}
interface ISelectServerViewState {
servers: IServer[];
}
interface ISelectServerViewProps {
navigation: StackNavigationProp<any, 'SelectServerView'>;
server: string;
}
class SelectServerView extends React.Component<ISelectServerViewProps, ISelectServerViewState> {
static navigationOptions = (): StackNavigationOptions => ({
title: I18n.t('Select_Server')
});
static propTypes = {
server: PropTypes.string,
navigation: PropTypes.object
};
state = { servers: [] };
state = { servers: [] as IServer[] };
async componentDidMount() {
const serversDB = database.servers;
const serversCollection = serversDB.get('servers');
const servers = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetch();
const servers: IServer[] = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetch();
this.setState({ servers });
}
select = async server => {
select = async (server: string) => {
const { server: currentServer, navigation } = this.props;
navigation.navigate('ShareListView');
@ -43,7 +53,7 @@ class SelectServerView extends React.Component {
}
};
renderItem = ({ item }) => {
renderItem = ({ item }: { item: IServer }) => {
const { server } = this.props;
return <ServerItem onPress={() => this.select(item.id)} item={item} hasCheck={item.id === server} />;
};
@ -62,7 +72,6 @@ class SelectServerView extends React.Component {
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={List.Separator}
enableEmptySections
removeClippedSubviews
keyboardShouldPersistTaps='always'
/>
@ -71,7 +80,7 @@ class SelectServerView extends React.Component {
}
}
const mapStateToProps = ({ share }) => ({
const mapStateToProps = ({ share }: any) => ({
server: share.server.server
});

View File

@ -1,8 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { Dispatch } from 'redux';
import { ScrollView, StyleSheet, Text } from 'react-native';
import { connect } from 'react-redux';
import Orientation from 'react-native-orientation-locker';
import { RouteProp } from '@react-navigation/native';
import { loginRequest as loginRequestAction } from '../actions/login';
import TextInput from '../containers/TextInput';
@ -27,21 +29,27 @@ const styles = StyleSheet.create({
}
});
class SetUsernameView extends React.Component {
static navigationOptions = ({ route }) => ({
interface ISetUsernameViewState {
username: string;
saving: boolean;
}
interface ISetUsernameViewProps {
navigation: StackNavigationProp<any, 'SetUsernameView'>;
route: RouteProp<{ SetUsernameView: { title: string } }, 'SetUsernameView'>;
server: string;
userId: string;
loginRequest: ({ resume }: { resume: string }) => void;
token: string;
theme: string;
}
class SetUsernameView extends React.Component<ISetUsernameViewProps, ISetUsernameViewState> {
static navigationOptions = ({ route }: Pick<ISetUsernameViewProps, 'route'>): StackNavigationOptions => ({
title: route.params?.title
});
static propTypes = {
navigation: PropTypes.object,
server: PropTypes.string,
userId: PropTypes.string,
loginRequest: PropTypes.func,
token: PropTypes.string,
theme: PropTypes.string
};
constructor(props) {
constructor(props: ISetUsernameViewProps) {
super(props);
this.state = {
username: '',
@ -61,7 +69,7 @@ class SetUsernameView extends React.Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdate(nextProps: ISetUsernameViewProps, nextState: ISetUsernameViewState) {
const { username, saving } = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
@ -88,7 +96,7 @@ class SetUsernameView extends React.Component {
try {
await RocketChat.saveUserProfile({ username });
await loginRequest({ resume: token });
} catch (e) {
} catch (e: any) {
showErrorAlert(e.message, I18n.t('Oops'));
}
this.setState({ saving: false });
@ -136,13 +144,13 @@ class SetUsernameView extends React.Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
server: state.server.server,
token: getUserSelector(state).token
});
const mapDispatchToProps = dispatch => ({
loginRequest: params => dispatch(loginRequestAction(params))
const mapDispatchToProps = (dispatch: Dispatch) => ({
loginRequest: (params: { resume: string }) => dispatch(loginRequestAction(params))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SetUsernameView));

View File

@ -1,5 +1,4 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Keyboard, StyleSheet, View } from 'react-native';
import ShareExtension from 'rn-extensions-share';
@ -8,6 +7,7 @@ import * as HeaderButton from '../../../containers/HeaderButton';
import { themes } from '../../../constants/colors';
import sharedStyles from '../../Styles';
import { animateNextTransition } from '../../../utils/layoutAnimation';
import { IShareListHeaderIos } from './interface';
const styles = StyleSheet.create({
container: {
@ -16,10 +16,10 @@ const styles = StyleSheet.create({
}
});
const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSearch, theme }) => {
const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSearch, theme }: IShareListHeaderIos) => {
const [text, setText] = useState('');
const onChangeText = searchText => {
const onChangeText = (searchText: string) => {
onChangeSearchText(searchText);
setText(searchText);
};
@ -59,12 +59,4 @@ const Header = React.memo(({ searching, onChangeSearchText, initSearch, cancelSe
);
});
Header.propTypes = {
searching: PropTypes.bool,
onChangeSearchText: PropTypes.func,
initSearch: PropTypes.func,
cancelSearch: PropTypes.func,
theme: PropTypes.string
};
export default Header;

View File

@ -1,11 +1,11 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import TextInput from '../../../presentation/TextInput';
import I18n from '../../../i18n';
import { themes } from '../../../constants/colors';
import sharedStyles from '../../Styles';
import { IShareListHeader } from './interface';
const styles = StyleSheet.create({
container: {
@ -24,7 +24,7 @@ const styles = StyleSheet.create({
}
});
const Header = React.memo(({ searching, onChangeSearchText, theme }) => {
const Header = React.memo(({ searching, onChangeSearchText, theme }: IShareListHeader) => {
const titleColorStyle = { color: themes[theme].headerTintColor };
const isLight = theme === 'light';
if (searching) {
@ -43,10 +43,4 @@ const Header = React.memo(({ searching, onChangeSearchText, theme }) => {
return <Text style={[styles.title, titleColorStyle]}>{I18n.t('Send_to')}</Text>;
});
Header.propTypes = {
searching: PropTypes.bool,
onChangeSearchText: PropTypes.func,
theme: PropTypes.string
};
export default Header;

View File

@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import Header from './Header';
import { IShareListHeader } from './interface';
const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, search, theme }) => {
const onSearchChangeText = text => {
search(text.trim());
const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, onChangeSearchText, theme }: IShareListHeader) => {
const onSearchChangeText = (text: string) => {
onChangeSearchText(text.trim());
};
return (
@ -19,12 +19,4 @@ const ShareListHeader = React.memo(({ searching, initSearch, cancelSearch, searc
);
});
ShareListHeader.propTypes = {
searching: PropTypes.bool,
initSearch: PropTypes.func,
cancelSearch: PropTypes.func,
search: PropTypes.func,
theme: PropTypes.string
};
export default ShareListHeader;

View File

@ -0,0 +1,13 @@
import { TextInputProps } from 'react-native';
type RequiredOnChangeText = Required<Pick<TextInputProps, 'onChangeText'>>;
export interface IShareListHeader {
searching: boolean;
onChangeSearchText: RequiredOnChangeText['onChangeText'];
theme: string;
initSearch?: () => void;
cancelSearch?: () => void;
}
export type IShareListHeaderIos = Required<IShareListHeader>;

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { BackHandler, FlatList, Keyboard, PermissionsAndroid, ScrollView, Text, View } from 'react-native';
import { StackNavigationProp } from '@react-navigation/stack';
import { BackHandler, FlatList, Keyboard, PermissionsAndroid, ScrollView, Text, View, Rationale } from 'react-native';
import ShareExtension from 'rn-extensions-share';
import * as FileSystem from 'expo-file-system';
import { connect } from 'react-redux';
@ -25,24 +25,75 @@ import { sanitizeLikeString } from '../../lib/database/utils';
import styles from './styles';
import ShareListHeader from './Header';
const permission = {
interface IFile {
value: string;
type: string;
}
interface IAttachment {
filename: string;
description: string;
size: number;
mime: any;
path: string;
}
interface IChat {
rid: string;
t: string;
name: string;
fname: string;
blocked: boolean;
blocker: boolean;
prid: string;
uids: string[];
usernames: string[];
topic: string;
description: string;
}
interface IServerInfo {
useRealName: boolean;
}
interface IState {
searching: boolean;
searchText: string;
searchResults: IChat[];
chats: IChat[];
serversCount: number;
attachments: IAttachment[];
text: string;
loading: boolean;
serverInfo: IServerInfo;
needsPermission: boolean;
}
interface INavigationOption {
navigation: StackNavigationProp<any, 'ShareListView'>;
}
interface IShareListViewProps extends INavigationOption {
server: string;
token: string;
userId: string;
theme: string;
}
const permission: Rationale = {
title: I18n.t('Read_External_Permission'),
message: I18n.t('Read_External_Permission_Message')
message: I18n.t('Read_External_Permission_Message'),
buttonPositive: 'Ok'
};
const getItemLayout = (data, index) => ({ length: data.length, offset: ROW_HEIGHT * index, index });
const keyExtractor = item => item.rid;
const getItemLayout = (data: any, index: number) => ({ length: data.length, offset: ROW_HEIGHT * index, index });
const keyExtractor = (item: IChat) => item.rid;
class ShareListView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
server: PropTypes.string,
token: PropTypes.string,
userId: PropTypes.string,
theme: PropTypes.string
};
class ShareListView extends React.Component<IShareListViewProps, IState> {
private unsubscribeFocus: (() => void) | undefined;
constructor(props) {
private unsubscribeBlur: (() => void) | undefined;
constructor(props: IShareListViewProps) {
super(props);
this.state = {
searching: false,
@ -53,7 +104,7 @@ class ShareListView extends React.Component {
attachments: [],
text: '',
loading: true,
serverInfo: null,
serverInfo: {} as IServerInfo,
needsPermission: isAndroid || false
};
this.setHeader();
@ -70,7 +121,7 @@ class ShareListView extends React.Component {
async componentDidMount() {
const { server } = this.props;
try {
const data = await ShareExtension.data();
const data = (await ShareExtension.data()) as IFile[];
if (isAndroid) {
await this.askForPermission(data);
}
@ -85,7 +136,7 @@ class ShareListView extends React.Component {
size: file.size,
mime: mime.lookup(file.uri),
path: file.uri
}));
})) as IAttachment[];
const text = data.filter(item => item.type === 'text').reduce((acc, item) => `${item.value}\n${acc}`, '');
this.setState({
text,
@ -98,14 +149,14 @@ class ShareListView extends React.Component {
this.getSubscriptions(server);
}
UNSAFE_componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps: IShareListViewProps) {
const { server } = this.props;
if (nextProps.server !== server) {
this.getSubscriptions(nextProps.server);
}
}
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdate(nextProps: IShareListViewProps, nextState: IState) {
const { searching, needsPermission } = this.state;
if (nextState.searching !== searching) {
return true;
@ -151,7 +202,7 @@ class ShareListView extends React.Component {
searching={searching}
initSearch={this.initSearch}
cancelSearch={this.cancelSearch}
search={this.search}
onChangeSearchText={this.search}
theme={theme}
/>
)
@ -168,7 +219,7 @@ class ShareListView extends React.Component {
) : (
<HeaderButton.CancelModal onPress={ShareExtension.close} testID='share-extension-close' />
),
headerTitle: () => <ShareListHeader searching={searching} search={this.search} theme={theme} />,
headerTitle: () => <ShareListHeader searching={searching} onChangeSearchText={this.search} theme={theme} />,
headerRight: () =>
searching ? null : (
<HeaderButton.Container>
@ -178,16 +229,16 @@ class ShareListView extends React.Component {
});
};
// eslint-disable-next-line react/sort-comp
internalSetState = (...args) => {
internalSetState = (...args: object[]) => {
const { navigation } = this.props;
if (navigation.isFocused()) {
animateNextTransition();
}
// @ts-ignore
this.setState(...args);
};
query = async text => {
query = async (text?: string) => {
const db = database.active;
const defaultWhereClause = [
Q.where('archived', false),
@ -195,15 +246,16 @@ class ShareListView extends React.Component {
Q.experimentalSkip(0),
Q.experimentalTake(20),
Q.experimentalSortBy('room_updated_at', Q.desc)
];
] as (Q.WhereDescription | Q.Skip | Q.Take | Q.SortBy | Q.Or)[];
if (text) {
const likeString = sanitizeLikeString(text);
defaultWhereClause.push(Q.or(Q.where('name', Q.like(`%${likeString}%`)), Q.where('fname', Q.like(`%${likeString}%`))));
}
const data = await db
const data = (await db
.get('subscriptions')
.query(...defaultWhereClause)
.fetch();
.fetch()) as IChat[];
return data.map(item => ({
rid: item.rid,
t: item.t,
@ -218,7 +270,7 @@ class ShareListView extends React.Component {
}));
};
getSubscriptions = async server => {
getSubscriptions = async (server: string) => {
const serversDB = database.servers;
if (server) {
@ -242,7 +294,7 @@ class ShareListView extends React.Component {
}
};
askForPermission = async data => {
askForPermission = async (data: IFile[]) => {
const mediaIndex = data.findIndex(item => item.type === 'media');
if (mediaIndex !== -1) {
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, permission);
@ -255,15 +307,14 @@ class ShareListView extends React.Component {
return Promise.resolve();
};
uriToPath = uri => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri);
uriToPath = (uri: string) => decodeURIComponent(isIOS ? uri.replace(/^file:\/\//, '') : uri);
getRoomTitle = item => {
getRoomTitle = (item: IChat) => {
const { serverInfo } = this.state;
const { useRealName } = serverInfo;
return ((item.prid || useRealName) && item.fname) || item.name;
return ((item.prid || serverInfo?.useRealName) && item.fname) || item.name;
};
shareMessage = room => {
shareMessage = (room: IChat) => {
const { attachments, text, serverInfo } = this.state;
const { navigation } = this.props;
@ -276,7 +327,7 @@ class ShareListView extends React.Component {
});
};
search = async text => {
search = async (text: string) => {
const result = await this.query(text);
this.internalSetState({
searchResults: result,
@ -303,7 +354,7 @@ class ShareListView extends React.Component {
return false;
};
renderSectionHeader = header => {
renderSectionHeader = (header: string) => {
const { searching } = this.state;
const { theme } = this.props;
if (searching) {
@ -320,10 +371,9 @@ class ShareListView extends React.Component {
);
};
renderItem = ({ item }) => {
renderItem = ({ item }: { item: IChat }) => {
const { serverInfo } = this.state;
const { useRealName } = serverInfo;
const { userId, token, server, theme } = this.props;
const { theme } = this.props;
let description;
switch (item.t) {
case 'c':
@ -333,7 +383,7 @@ class ShareListView extends React.Component {
description = item.topic || item.description;
break;
case 'd':
description = useRealName ? item.name : item.fname;
description = serverInfo?.useRealName ? item.name : item.fname;
break;
default:
description = item.fname;
@ -341,12 +391,7 @@ class ShareListView extends React.Component {
}
return (
<DirectoryItem
user={{
id: userId,
token
}}
title={this.getRoomTitle(item)}
baseUrl={server}
avatar={RocketChat.getRoomAvatar(item)}
description={description}
type={item.prid ? 'discussion' : item.t}
@ -439,7 +484,7 @@ class ShareListView extends React.Component {
};
}
const mapStateToProps = ({ share }) => ({
const mapStateToProps = ({ share }: any) => ({
userId: share.user && share.user.id,
token: share.user && share.user.token,
server: share.server.server

View File

@ -25,7 +25,7 @@ const Item = React.memo(({ left, right, text, onPress, testID, current, theme }:
style={[styles.item, current && { backgroundColor: themes[theme].borderColor }]}>
<View style={styles.itemHorizontal}>{left}</View>
<View style={styles.itemCenter}>
<Text style={[styles.itemText, { color: themes[theme].titleText }]} numberOfLines={1}>
<Text style={[styles.itemText, { color: themes[theme].titleText }]} numberOfLines={1} accessibilityLabel={text}>
{text}
</Text>
</View>

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StackNavigationProp } from '@react-navigation/stack';
import { FlatList, StyleSheet } from 'react-native';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import I18n from '../i18n';
@ -53,23 +54,29 @@ const styles = StyleSheet.create({
}
});
class StatusView extends React.Component {
static propTypes = {
user: PropTypes.shape({
id: PropTypes.string,
status: PropTypes.string,
statusText: PropTypes.string
}),
theme: PropTypes.string,
navigation: PropTypes.object,
isMasterDetail: PropTypes.bool,
setUser: PropTypes.func,
Accounts_AllowInvisibleStatusOption: PropTypes.bool
};
interface IUser {
id: string;
status: string;
statusText: string;
}
constructor(props) {
interface IStatusViewState {
statusText: string;
loading: boolean;
}
interface IStatusViewProps {
navigation: StackNavigationProp<any, 'StatusView'>;
user: IUser;
theme: string;
isMasterDetail: boolean;
setUser: (user: Partial<IUser>) => void;
Accounts_AllowInvisibleStatusOption: boolean;
}
class StatusView extends React.Component<IStatusViewProps, IStatusViewState> {
constructor(props: IStatusViewProps) {
super(props);
const { statusText } = props.user;
this.state = { statusText: statusText || '', loading: false };
this.setHeader();
@ -103,7 +110,7 @@ class StatusView extends React.Component {
navigation.goBack();
};
setCustomStatus = async statusText => {
setCustomStatus = async (statusText: string) => {
const { user, setUser } = this.props;
this.setState({ loading: true });
@ -147,7 +154,7 @@ class StatusView extends React.Component {
);
};
renderItem = ({ item }) => {
renderItem = ({ item }: { item: { id: string; name: string } }) => {
const { statusText } = this.state;
const { user, setUser } = this.props;
const { id, name } = item;
@ -155,6 +162,7 @@ class StatusView extends React.Component {
<List.Item
title={name}
onPress={async () => {
// @ts-ignore
logEvent(events[`STATUS_${item.id.toUpperCase()}`]);
if (user.status !== item.id) {
try {
@ -162,7 +170,7 @@ class StatusView extends React.Component {
if (result.success) {
setUser({ status: item.id });
}
} catch (e) {
} catch (e: any) {
showErrorAlert(I18n.t(e.data.errorType));
logEvent(events.SET_STATUS_FAIL);
log(e);
@ -197,14 +205,14 @@ class StatusView extends React.Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
user: getUserSelector(state),
isMasterDetail: state.app.isMasterDetail,
Accounts_AllowInvisibleStatusOption: state.settings.Accounts_AllowInvisibleStatusOption ?? true
});
const mapDispatchToProps = dispatch => ({
setUser: user => dispatch(setUserAction(user))
const mapDispatchToProps = (dispatch: Dispatch) => ({
setUser: (user: IUser) => dispatch(setUserAction(user))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(StatusView));

View File

@ -1,10 +1,11 @@
import React from 'react';
import { Alert, FlatList, Keyboard } from 'react-native';
import PropTypes from 'prop-types';
import { RouteProp } from '@react-navigation/native';
import { Dispatch } from 'redux';
import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
import { connect } from 'react-redux';
import { HeaderBackButton } from '@react-navigation/stack';
import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import StatusBar from '../containers/StatusBar';
import RoomHeader from '../containers/RoomHeader';
@ -38,35 +39,74 @@ const PERMISSION_EDIT_TEAM_CHANNEL = 'edit-team-channel';
const PERMISSION_REMOVE_TEAM_CHANNEL = 'remove-team-channel';
const PERMISSION_ADD_TEAM_CHANNEL = 'add-team-channel';
const getItemLayout = (data, index) => ({
length: data.length,
const getItemLayout = (data: IItem[] | null | undefined, index: number) => ({
length: data?.length || 0,
offset: ROW_HEIGHT * index,
index
});
const keyExtractor = item => item._id;
const keyExtractor = (item: IItem) => item._id;
class TeamChannelsView extends React.Component {
static propTypes = {
route: PropTypes.object,
navigation: PropTypes.object,
isMasterDetail: PropTypes.bool,
insets: PropTypes.object,
theme: PropTypes.string,
useRealName: PropTypes.bool,
width: PropTypes.number,
StoreLastMessage: PropTypes.bool,
addTeamChannelPermission: PropTypes.array,
editTeamChannelPermission: PropTypes.array,
removeTeamChannelPermission: PropTypes.array,
deleteCPermission: PropTypes.array,
deletePPermission: PropTypes.array,
showActionSheet: PropTypes.func,
deleteRoom: PropTypes.func,
showAvatar: PropTypes.bool,
displayMode: PropTypes.string
};
// This interface comes from request RocketChat.getTeamListRoom
interface IItem {
_id: string;
fname: string;
customFields: object;
broadcast: boolean;
encrypted: boolean;
name: string;
t: string;
msgs: number;
usersCount: number;
u: { _id: string; name: string };
ts: string;
ro: boolean;
teamId: string;
default: boolean;
sysMes: boolean;
_updatedAt: string;
teamDefault: boolean;
}
constructor(props) {
interface ITeamChannelsViewState {
loading: boolean;
loadingMore: boolean;
data: IItem[];
isSearching: boolean;
searchText: string | null;
search: IItem[];
end: boolean;
showCreate: boolean;
}
interface ITeamChannelsViewProps {
route: RouteProp<{ TeamChannelsView: { teamId: string } }, 'TeamChannelsView'>;
navigation: StackNavigationProp<any, 'TeamChannelsView'>;
isMasterDetail: boolean;
insets: EdgeInsets;
theme: string;
useRealName: boolean;
width: number;
StoreLastMessage: boolean;
addTeamChannelPermission: string[];
editTeamChannelPermission: string[];
removeTeamChannelPermission: string[];
deleteCPermission: string[];
deletePPermission: string[];
showActionSheet: (options: any) => void;
deleteRoom: (rid: string, t: string) => void;
showAvatar: boolean;
displayMode: string;
}
class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChannelsViewState> {
private teamId: string;
// TODO: Refactor when migrate room
private teamChannels: any;
// TODO: Refactor when migrate room
private team: any;
constructor(props: ITeamChannelsViewProps) {
super(props);
this.teamId = props.route.params?.teamId;
this.state = {
@ -95,7 +135,7 @@ class TeamChannelsView extends React.Component {
try {
const subCollection = db.get('subscriptions');
this.teamChannels = await subCollection.query(Q.where('team_id', Q.eq(this.teamId)));
this.team = this.teamChannels?.find(channel => channel.teamMain);
this.team = this.teamChannels?.find((channel: any) => channel.teamMain);
this.setHeader();
if (!this.team) {
@ -140,7 +180,7 @@ class TeamChannelsView extends React.Component {
loading: false,
loadingMore: false,
end: result.rooms.length < API_FETCH_COUNT
};
} as ITeamChannelsViewState;
if (isSearching) {
newState.search = [...search, ...result.rooms];
@ -170,7 +210,7 @@ class TeamChannelsView extends React.Component {
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 });
if (isSearching) {
const options = {
const options: StackNavigationOptions = {
headerTitleAlign: 'left',
headerLeft: () => (
<HeaderButton.Container left>
@ -187,7 +227,7 @@ class TeamChannelsView extends React.Component {
return navigation.setOptions(options);
}
const options = {
const options: StackNavigationOptions = {
headerShown: true,
headerTitleAlign: 'left',
headerTitleContainerStyle: {
@ -232,7 +272,7 @@ class TeamChannelsView extends React.Component {
this.setState({ isSearching: true }, () => this.setHeader());
};
onSearchChangeText = debounce(searchText => {
onSearchChangeText = debounce((searchText: string) => {
this.setState(
{
searchText,
@ -270,7 +310,7 @@ class TeamChannelsView extends React.Component {
);
};
goRoomActionsView = screen => {
goRoomActionsView = (screen: string) => {
logEvent(events.TC_GO_ACTIONS);
const { team } = this;
const { navigation, isMasterDetail } = this.props;
@ -293,12 +333,12 @@ class TeamChannelsView extends React.Component {
}
};
getRoomTitle = item => RocketChat.getRoomTitle(item);
getRoomTitle = (item: IItem) => RocketChat.getRoomTitle(item);
getRoomAvatar = item => RocketChat.getRoomAvatar(item);
getRoomAvatar = (item: IItem) => RocketChat.getRoomAvatar(item);
onPressItem = debounce(
async item => {
async (item: IItem) => {
logEvent(events.TC_GO_ROOM);
const { navigation, isMasterDetail } = this.props;
try {
@ -314,7 +354,7 @@ class TeamChannelsView extends React.Component {
navigation.pop();
}
goRoom({ item: params, isMasterDetail, navigationMethod: navigation.push });
} catch (e) {
} catch (e: any) {
if (e.data.error === 'not-allowed') {
showErrorAlert(I18n.t('error-not-allowed'));
} else {
@ -326,7 +366,7 @@ class TeamChannelsView extends React.Component {
true
);
toggleAutoJoin = async item => {
toggleAutoJoin = async (item: IItem) => {
logEvent(events.TC_TOGGLE_AUTOJOIN);
try {
const { data } = this.state;
@ -346,7 +386,7 @@ class TeamChannelsView extends React.Component {
}
};
remove = item => {
remove = (item: IItem) => {
Alert.alert(
I18n.t('Confirmation'),
I18n.t('Remove_Team_Room_Warning'),
@ -365,7 +405,7 @@ class TeamChannelsView extends React.Component {
);
};
removeRoom = async item => {
removeRoom = async (item: IItem) => {
logEvent(events.TC_DELETE_ROOM);
try {
const { data } = this.state;
@ -380,7 +420,7 @@ class TeamChannelsView extends React.Component {
}
};
delete = item => {
delete = (item: IItem) => {
logEvent(events.TC_DELETE_ROOM);
const { deleteRoom } = this.props;
@ -402,7 +442,7 @@ class TeamChannelsView extends React.Component {
);
};
showChannelActions = async item => {
showChannelActions = async (item: IItem) => {
logEvent(events.ROOM_SHOW_BOX_ACTIONS);
const {
showActionSheet,
@ -464,7 +504,7 @@ class TeamChannelsView extends React.Component {
showActionSheet({ options });
};
renderItem = ({ item }) => {
renderItem = ({ item }: { item: IItem }) => {
const { StoreLastMessage, useRealName, theme, width, showAvatar, displayMode } = this.props;
return (
<RoomItem
@ -534,7 +574,7 @@ class TeamChannelsView extends React.Component {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
baseUrl: state.server.server,
user: getUserSelector(state),
useRealName: state.settings.UI_Use_Real_Name,
@ -549,8 +589,8 @@ const mapStateToProps = state => ({
displayMode: state.sortPreferences.displayMode
});
const mapDispatchToProps = dispatch => ({
deleteRoom: (rid, t) => dispatch(deleteRoomAction(rid, t))
const mapDispatchToProps = (dispatch: Dispatch) => ({
deleteRoom: (rid: string, t: string) => dispatch(deleteRoomAction(rid, t))
});
export default connect(

View File

@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StackNavigationOptions } from '@react-navigation/stack';
import I18n from '../i18n';
import { withTheme } from '../theme';
@ -51,18 +51,29 @@ if (supportSystemTheme()) {
const themeGroup = THEMES.filter(item => item.group === THEME_GROUP);
const darkGroup = THEMES.filter(item => item.group === DARK_GROUP);
class ThemeView extends React.Component {
static navigationOptions = () => ({
interface ITheme {
label: string;
value: string;
group: string;
}
interface IThemePreference {
currentTheme?: string;
darkLevel?: string;
}
interface IThemeViewProps {
theme: string;
themePreferences: IThemePreference;
setTheme(newTheme?: IThemePreference): void;
}
class ThemeView extends React.Component<IThemeViewProps> {
static navigationOptions = (): StackNavigationOptions => ({
title: I18n.t('Theme')
});
static propTypes = {
theme: PropTypes.string,
themePreferences: PropTypes.object,
setTheme: PropTypes.func
};
isSelected = item => {
isSelected = (item: ITheme) => {
const { themePreferences } = this.props;
const { group } = item;
const { darkLevel, currentTheme } = themePreferences;
@ -74,11 +85,11 @@ class ThemeView extends React.Component {
}
};
onClick = item => {
onClick = (item: ITheme) => {
const { themePreferences } = this.props;
const { darkLevel, currentTheme } = themePreferences;
const { value, group } = item;
let changes = {};
let changes: IThemePreference = {};
if (group === THEME_GROUP && currentTheme !== value) {
logEvent(events.THEME_SET_THEME_GROUP, { theme_group: value });
changes = { currentTheme: value };
@ -90,7 +101,7 @@ class ThemeView extends React.Component {
this.setTheme(changes);
};
setTheme = async theme => {
setTheme = async (theme: IThemePreference) => {
const { setTheme, themePreferences } = this.props;
const newTheme = { ...themePreferences, ...theme };
setTheme(newTheme);
@ -102,7 +113,7 @@ class ThemeView extends React.Component {
return <List.Icon name='check' color={themes[theme].tintColor} />;
};
renderItem = ({ item }) => {
renderItem = ({ item }: { item: ITheme }) => {
const { label, value } = item;
return (
<>

View File

@ -1,10 +1,33 @@
const { exec } = require('child_process');
const data = require('../data');
const platformTypes = {
android: {
// Android types
alertButtonType: 'android.widget.Button',
scrollViewType: 'android.widget.ScrollView',
textInputType: 'android.widget.EditText',
textMatcher: 'text'
},
ios: {
// iOS types
alertButtonType: '_UIAlertControllerActionView',
scrollViewType: 'UIScrollView',
textInputType: '_UIAlertControllerTextField',
textMatcher: 'label'
}
};
function sleep(ms) {
return new Promise(res => setTimeout(res, ms));
}
async function navigateToWorkspace(server = data.server) {
await waitFor(element(by.id('new-server-view')))
.toBeVisible()
.withTimeout(60000);
await element(by.id('new-server-view-input')).typeText(`${server}\n`);
await element(by.id('new-server-view-input')).replaceText(`${server}`);
await element(by.id('new-server-view-input')).tapReturnKey();
await waitFor(element(by.id('workspace-view')))
.toBeVisible()
.withTimeout(60000);
@ -41,6 +64,8 @@ async function login(username, password) {
}
async function logout() {
const deviceType = device.getPlatform();
const { scrollViewType, textMatcher } = platformTypes[deviceType];
await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar-view')))
.toBeVisible()
@ -52,14 +77,14 @@ async function logout() {
await waitFor(element(by.id('settings-view')))
.toBeVisible()
.withTimeout(2000);
await element(by.type('UIScrollView')).atIndex(1).scrollTo('bottom');
await element(by.type(scrollViewType)).atIndex(1).scrollTo('bottom');
await element(by.id('settings-logout')).tap();
const logoutAlertMessage = 'You will be logged out of this application.';
await waitFor(element(by.text(logoutAlertMessage)).atIndex(0))
await waitFor(element(by[textMatcher](logoutAlertMessage)).atIndex(0))
.toExist()
.withTimeout(10000);
await expect(element(by.text(logoutAlertMessage)).atIndex(0)).toExist();
await element(by.text('Logout')).tap();
await expect(element(by[textMatcher](logoutAlertMessage)).atIndex(0)).toExist();
await element(by[textMatcher]('Logout')).atIndex(0).tap();
await waitFor(element(by.id('new-server-view')))
.toBeVisible()
.withTimeout(10000);
@ -67,66 +92,73 @@ async function logout() {
}
async function mockMessage(message, isThread = false) {
const deviceType = device.getPlatform();
const { textMatcher } = platformTypes[deviceType];
const input = isThread ? 'messagebox-input-thread' : 'messagebox-input';
await element(by.id(input)).tap();
await element(by.id(input)).typeText(`${data.random}${message}`);
await element(by.id(input)).replaceText(`${data.random}${message}`);
await sleep(300);
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.label(`${data.random}${message}`)))
await waitFor(element(by[textMatcher](`${data.random}${message}`)))
.toExist()
.withTimeout(60000);
await expect(element(by.label(`${data.random}${message}`))).toExist();
await element(by.label(`${data.random}${message}`))
await element(by[textMatcher](`${data.random}${message}`))
.atIndex(0)
.tap();
}
async function starMessage(message) {
const deviceType = device.getPlatform();
const { textMatcher } = platformTypes[deviceType];
const messageLabel = `${data.random}${message}`;
await element(by.label(messageLabel)).atIndex(0).longPress();
await element(by[textMatcher](messageLabel)).atIndex(0).longPress();
await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Star')).atIndex(0).tap();
await element(by[textMatcher]('Star')).atIndex(0).tap();
await waitFor(element(by.id('action-sheet')))
.not.toExist()
.withTimeout(5000);
}
async function pinMessage(message) {
const deviceType = device.getPlatform();
const { textMatcher } = platformTypes[deviceType];
const messageLabel = `${data.random}${message}`;
await waitFor(element(by.label(messageLabel)).atIndex(0)).toExist();
await element(by.label(messageLabel)).atIndex(0).longPress();
await waitFor(element(by[textMatcher](messageLabel)).atIndex(0)).toExist();
await element(by[textMatcher](messageLabel)).atIndex(0).longPress();
await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Pin')).atIndex(0).tap();
await element(by[textMatcher]('Pin')).atIndex(0).tap();
await waitFor(element(by.id('action-sheet')))
.not.toExist()
.withTimeout(5000);
}
async function dismissReviewNag() {
await waitFor(element(by.text('Are you enjoying this app?')))
const deviceType = device.getPlatform();
const { textMatcher } = platformTypes[deviceType];
await waitFor(element(by[textMatcher]('Are you enjoying this app?')))
.toExist()
.withTimeout(60000);
await element(by.label('No').and(by.type('_UIAlertControllerActionView'))).tap(); // Tap `no` on ask for review alert
await element(by[textMatcher]('No')).atIndex(0).tap(); // Tap `no` on ask for review alert
}
async function tapBack() {
await element(by.id('header-back')).atIndex(0).tap();
}
function sleep(ms) {
return new Promise(res => setTimeout(res, ms));
}
async function searchRoom(room) {
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
.withTimeout(30000);
await element(by.id('rooms-list-view-search')).tap();
await expect(element(by.id('rooms-list-view-search-input'))).toExist();
await waitFor(element(by.id('rooms-list-view-search-input')))
.toExist()
.withTimeout(5000);
await element(by.id('rooms-list-view-search-input')).typeText(room);
await sleep(300);
await element(by.id('rooms-list-view-search-input')).replaceText(room);
await sleep(300);
await waitFor(element(by.id(`rooms-list-view-item-${room}`)))
.toBeVisible()
@ -162,6 +194,29 @@ const checkServer = async server => {
await element(by.id('sidebar-close-drawer')).tap();
};
function runCommand(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(new Error(`exec error: ${stderr}`));
return;
}
resolve();
});
});
}
async function prepareAndroid() {
if (device.getPlatform() !== 'android') {
return;
}
await runCommand('adb shell settings put secure spell_checker_enabled 0');
await runCommand('adb shell settings put secure autofill_service null');
await runCommand('adb shell settings put global window_animation_scale 0.0');
await runCommand('adb shell settings put global transition_animation_scale 0.0');
await runCommand('adb shell settings put global animator_duration_scale 0.0');
}
module.exports = {
navigateToWorkspace,
navigateToLogin,
@ -176,5 +231,7 @@ module.exports = {
sleep,
searchRoom,
tryTapping,
checkServer
checkServer,
platformTypes,
prepareAndroid
};

View File

@ -1,4 +1,5 @@
const { navigateToLogin, login, sleep, tapBack, mockMessage, searchRoom, logout } = require('../../helpers/app');
const { navigateToLogin, login, sleep, tapBack, mockMessage, searchRoom, logout, platformTypes } = require('../../helpers/app');
const data = require('../../data');
const testuser = data.users.regular;
@ -17,8 +18,9 @@ const checkServer = async server => {
};
const checkBanner = async () => {
await waitFor(element(by.id('listheader-encryption').withDescendant(by.label('Save Your Encryption Password'))))
.toBeVisible()
// TODO: Assert 'Save Your Encryption Password'
await waitFor(element(by.id('listheader-encryption')))
.toExist()
.withTimeout(10000);
};
@ -58,9 +60,13 @@ async function navigateSecurityPrivacy() {
describe('E2E Encryption', () => {
const room = `encrypted${data.random}`;
const newPassword = 'abc';
let alertButtonType;
let scrollViewType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, scrollViewType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(testuser.username, testuser.password);
});
@ -187,11 +193,11 @@ describe('E2E Encryption', () => {
it('should change password', async () => {
await element(by.id('e2e-encryption-security-view-password')).typeText(newPassword);
await element(by.id('e2e-encryption-security-view-change-password')).tap();
await waitFor(element(by.text('Are you sure?')))
await waitFor(element(by[textMatcher]('Are you sure?')))
.toExist()
.withTimeout(2000);
await expect(element(by.text("Make sure you've saved it carefully somewhere else."))).toExist();
await element(by.label('Yes, change it').and(by.type('_UIAlertControllerActionView'))).tap();
await expect(element(by[textMatcher]("Make sure you've saved it carefully somewhere else."))).toExist();
await element(by[textMatcher]('Yes, change it')).atIndex(0).tap();
await waitForToast();
});
@ -216,7 +222,7 @@ describe('E2E Encryption', () => {
.toBeVisible()
.withTimeout(2000);
await navigateToRoom(room);
await waitFor(element(by.label(`${data.random}message`)).atIndex(0))
await waitFor(element(by[textMatcher](`${data.random}message`)).atIndex(0))
.toExist()
.withTimeout(2000);
});
@ -230,7 +236,7 @@ describe('E2E Encryption', () => {
await navigateToLogin();
await login(testuser.username, testuser.password);
await navigateToRoom(room);
await waitFor(element(by.label(`${data.random}message`)).atIndex(0))
await waitFor(element(by[textMatcher](`${data.random}message`)).atIndex(0))
.not.toExist()
.withTimeout(2000);
await expect(element(by.label('Encrypted message')).atIndex(0)).toExist();
@ -241,10 +247,11 @@ describe('E2E Encryption', () => {
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
.withTimeout(2000);
await waitFor(element(by.id('listheader-encryption').withDescendant(by.label('Enter Your E2E Password'))))
// TODO: assert 'Enter Your E2E Password'
await waitFor(element(by.id('listheader-encryption')))
.toBeVisible()
.withTimeout(2000);
await element(by.id('listheader-encryption').withDescendant(by.label('Enter Your E2E Password'))).tap();
await element(by.id('listheader-encryption')).tap();
await waitFor(element(by.id('e2e-enter-your-password-view')))
.toBeVisible()
.withTimeout(2000);
@ -254,43 +261,52 @@ describe('E2E Encryption', () => {
.not.toExist()
.withTimeout(10000);
await navigateToRoom(room);
await waitFor(element(by.label(`${data.random}message`)).atIndex(0))
await waitFor(element(by[textMatcher](`${data.random}message`)).atIndex(0))
.toExist()
.withTimeout(2000);
});
});
describe('Reset E2E key', () => {
it('should reset e2e key', async () => {
before(async () => {
await tapBack();
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
.withTimeout(2000);
});
it('should reset e2e key', async () => {
// FIXME: too flaky on Android for now... let's fix it later
// It's also flaky on iOS, but it works from time to time
if (device.getPlatform() === 'android') {
return;
}
await navigateSecurityPrivacy();
await element(by.id('security-privacy-view-e2e-encryption')).tap();
await waitFor(element(by.id('e2e-encryption-security-view')))
.toBeVisible()
.withTimeout(2000);
await element(by.id('e2e-encryption-security-view-reset-key').and(by.label('Reset E2E Key'))).tap();
await waitFor(element(by.text('Are you sure?')))
await waitFor(element(by[textMatcher]('Are you sure?')))
.toExist()
.withTimeout(2000);
await expect(element(by.text("You're going to be logged out."))).toExist();
await element(by.label('Yes, reset it').and(by.type('UILabel'))).tap();
await expect(element(by[textMatcher]("You're going to be logged out."))).toExist();
await element(by[textMatcher]('Yes, reset it').and(by.type(alertButtonType))).tap();
await sleep(2000);
await waitFor(element(by[textMatcher]("You've been logged out by the server. Please log in again.")))
.toExist()
.withTimeout(20000);
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('workspace-view')))
.toBeVisible()
.withTimeout(10000);
await waitFor(element(by.text("You've been logged out by the server. Please log in again.")))
.toExist()
.withTimeout(2000);
await element(by.label('OK').and(by.type('_UIAlertControllerActionView'))).tap();
await element(by.id('workspace-view-login')).tap();
await waitFor(element(by.id('login-view')))
.toBeVisible()
.withTimeout(2000);
await login(testuser.username, testuser.password);
await waitFor(element(by.id('listheader-encryption').withDescendant(by.label('Save Your Encryption Password'))))
// TODO: assert 'Save Your Encryption Password'
await waitFor(element(by.id('listheader-encryption')))
.toBeVisible()
.withTimeout(2000);
});
@ -298,6 +314,14 @@ describe('E2E Encryption', () => {
});
describe('Persist Banner', () => {
before(async () => {
// reinstall the app because of one flaky test above
if (device.getPlatform() === 'android') {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(testuser.username, testuser.password);
}
});
it('check save banner', async () => {
await checkServer(data.server);
await checkBanner();
@ -315,7 +339,8 @@ describe('E2E Encryption', () => {
await waitFor(element(by.id('new-server-view')))
.toBeVisible()
.withTimeout(60000);
await element(by.id('new-server-view-input')).typeText(`${data.alternateServer}\n`);
await element(by.id('new-server-view-input')).typeText(`${data.alternateServer}`);
await element(by.id('new-server-view-input')).tapReturnKey();
await waitFor(element(by.id('workspace-view')))
.toBeVisible()
.withTimeout(60000);
@ -328,7 +353,7 @@ describe('E2E Encryption', () => {
await element(by.id('register-view-name')).replaceText(data.registeringUser.username);
await element(by.id('register-view-username')).replaceText(data.registeringUser.username);
await element(by.id('register-view-email')).replaceText(data.registeringUser.email);
await element(by.id('register-view-password')).typeText(data.registeringUser.password);
await element(by.id('register-view-password')).replaceText(data.registeringUser.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()

View File

@ -1,15 +1,17 @@
// const OTP = require('otp.js');
// const GA = OTP.googleAuthenticator;
const { navigateToLogin, login, mockMessage, tapBack, searchRoom } = require('../../helpers/app');
const { navigateToLogin, login, mockMessage, tapBack, searchRoom, platformTypes } = require('../../helpers/app');
const data = require('../../data');
const testuser = data.users.regular;
const otheruser = data.users.alternate;
describe('Broadcast room', () => {
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(testuser.username, testuser.password);
});
@ -101,7 +103,7 @@ describe('Broadcast room', () => {
});
it('should have the message created earlier', async () => {
await waitFor(element(by.label(`${data.random}message`)))
await waitFor(element(by[textMatcher](`${data.random}message`)))
.toExist()
.withTimeout(60000);
});

View File

@ -1,4 +1,4 @@
const { navigateToLogin, login, sleep } = require('../../helpers/app');
const { navigateToLogin, login, sleep, platformTypes } = require('../../helpers/app');
const data = require('../../data');
const profileChangeUser = data.users.profileChanges;
@ -14,8 +14,14 @@ async function waitForToast() {
}
describe('Profile screen', () => {
let textInputType;
let scrollViewType;
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ textInputType, scrollViewType, alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(profileChangeUser.username, profileChangeUser.password);
await element(by.id('rooms-list-view-sidebar')).tap();
@ -92,8 +98,8 @@ describe('Profile screen', () => {
describe('Usage', () => {
it('should change name and username', async () => {
await element(by.id('profile-view-name')).replaceText(`${profileChangeUser.username}new`);
await element(by.id('profile-view-username')).typeText(`${profileChangeUser.username}new`);
await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('profile-view-username')).replaceText(`${profileChangeUser.username}new`);
await element(by.type(scrollViewType)).atIndex(1).swipe('up');
await element(by.id('profile-view-submit')).tap();
await waitForToast();
});
@ -102,12 +108,13 @@ describe('Profile screen', () => {
await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${data.random}@rocket.chat`);
await element(by.id('profile-view-new-password')).replaceText(`${profileChangeUser.password}new`);
await element(by.id('profile-view-submit')).tap();
await element(by.type('_UIAlertControllerTextField')).typeText(`${profileChangeUser.password}\n`);
await element(by.type(textInputType)).replaceText(`${profileChangeUser.password}`);
await element(by[textMatcher]('Save').and(by.type(alertButtonType))).tap();
await waitForToast();
});
it('should reset avatar', async () => {
await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.type(scrollViewType)).atIndex(1).swipe('up');
await element(by.id('profile-view-reset-avatar')).tap();
await waitForToast();
});

View File

@ -1,11 +1,15 @@
const { navigateToLogin, login } = require('../../helpers/app');
const { navigateToLogin, login, platformTypes } = require('../../helpers/app');
const data = require('../../data');
const testuser = data.users.regular;
describe('Settings screen', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(testuser.username, testuser.password);
await waitFor(element(by.id('rooms-list-view')))
@ -72,10 +76,10 @@ describe('Settings screen', () => {
.toBeVisible()
.withTimeout(2000);
await element(by.id('settings-view-clear-cache')).tap();
await waitFor(element(by.text('This will clear all your offline data.')))
await waitFor(element(by[textMatcher]('This will clear all your offline data.')))
.toExist()
.withTimeout(2000);
await element(by.label('Clear').and(by.type('_UIAlertControllerActionView'))).tap();
await element(by[textMatcher]('Clear').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
.withTimeout(5000);

View File

@ -1,5 +1,5 @@
const data = require('../../data');
const { navigateToLogin, login, mockMessage, tapBack, searchRoom } = require('../../helpers/app');
const { navigateToLogin, login, mockMessage, tapBack, searchRoom, platformTypes } = require('../../helpers/app');
const testuser = data.users.regular;
const room = data.channels.detoxpublic.name;
@ -7,21 +7,24 @@ const room = data.channels.detoxpublic.name;
async function navigateToRoom() {
await searchRoom(room);
await element(by.id(`rooms-list-view-item-${room}`)).tap();
await waitFor(element(by.id('room-view')))
.toBeVisible()
await waitFor(element(by.id('room-view')).atIndex(0))
.toExist()
.withTimeout(5000);
}
async function navigateToRoomActions() {
await element(by.id('room-header')).tap();
await element(by.id(`room-view-title-${room}`)).tap();
await waitFor(element(by.id('room-actions-view')))
.toBeVisible()
.withTimeout(5000);
}
describe('Join public room', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(testuser.username, testuser.password);
await navigateToRoom();
@ -32,10 +35,6 @@ describe('Join public room', () => {
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', () => {
it('should have actions button ', async () => {
@ -75,16 +74,10 @@ describe('Join public room', () => {
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();
await waitFor(element(by.id('room-actions-members')))
.toBeVisible()
.withTimeout(2000);
});
it('should have files', async () => {
@ -147,32 +140,29 @@ describe('Join public room', () => {
await navigateToRoomActions();
await expect(element(by.id('room-actions-view'))).toBeVisible();
await expect(element(by.id('room-actions-info'))).toBeVisible();
// await expect(element(by.id('room-actions-voice'))).toBeVisible();
// await expect(element(by.id('room-actions-video'))).toBeVisible();
await expect(element(by.id('room-actions-members'))).toBeVisible();
await expect(element(by.id('room-actions-files'))).toBeVisible();
await expect(element(by.id('room-actions-mentioned'))).toBeVisible();
await expect(element(by.id('room-actions-starred'))).toBeVisible();
await element(by.id('room-actions-scrollview')).swipe('down');
await expect(element(by.id('room-actions-share'))).toBeVisible();
await expect(element(by.id('room-actions-pinned'))).toBeVisible();
await expect(element(by.id('room-actions-notifications'))).toBeVisible();
await element(by.id('room-actions-scrollview')).scrollTo('bottom');
await expect(element(by.id('room-actions-leave-channel'))).toBeVisible();
});
it('should leave room', async () => {
await element(by.id('room-actions-leave-channel')).tap();
await waitFor(element(by.text('Yes, leave it!')))
await waitFor(element(by[textMatcher]('Yes, leave it!').and(by.type(alertButtonType))))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.text('Yes, leave it!'))).toBeVisible();
await element(by.text('Yes, leave it!')).tap();
await element(by[textMatcher]('Yes, leave it!').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
.withTimeout(10000);
await waitFor(element(by.id(`rooms-list-view-item-${room}`)))
.toBeNotVisible()
.withTimeout(60000);
.withTimeout(60000); // flaky on Android
});
});
});

View File

@ -45,8 +45,9 @@ describe('Status screen', () => {
.withTimeout(2000);
});
// TODO: flaky
it('should change status text', async () => {
await element(by.id('status-view-input')).typeText('status-text-new');
await element(by.id('status-view-input')).replaceText('status-text-new');
await element(by.id('status-view-submit')).tap();
await waitForToast();
await waitFor(element(by.label('status-text-new').withAncestor(by.id('sidebar-custom-status'))))

View File

@ -1,8 +1,8 @@
const data = require('../../data');
const { navigateToLogin, login, checkServer } = require('../../helpers/app');
const { navigateToLogin, login, checkServer, platformTypes } = require('../../helpers/app');
const reopenAndCheckServer = async server => {
await device.launchApp({ permissions: { notifications: 'YES' } });
await device.launchApp({ permissions: { notifications: 'YES' }, newInstance: true });
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
.withTimeout(10000);
@ -37,7 +37,8 @@ describe('Change server', () => {
await waitFor(element(by.id('new-server-view')))
.toBeVisible()
.withTimeout(6000);
await element(by.id('new-server-view-input')).typeText(`${data.alternateServer}\n`);
await element(by.id('new-server-view-input')).replaceText(`${data.alternateServer}`);
await element(by.id('new-server-view-input')).tapReturnKey();
await waitFor(element(by.id('workspace-view')))
.toBeVisible()
.withTimeout(10000);

View File

@ -1,5 +1,5 @@
const data = require('../../data');
const { navigateToLogin, login, mockMessage, searchRoom } = require('../../helpers/app');
const { navigateToLogin, login, mockMessage, searchRoom, sleep } = require('../../helpers/app');
const testuser = data.users.regular;
const room = data.channels.detoxpublicprotected.name;
@ -9,15 +9,25 @@ async function navigateToRoom() {
await searchRoom(room);
await element(by.id(`rooms-list-view-item-${room}`)).tap();
await waitFor(element(by.id('room-view')))
.toBeVisible()
.toExist()
.withTimeout(5000);
}
async function openJoinCode() {
await element(by.id('room-view-join-button')).tap();
await waitFor(element(by.id('join-code')))
.toBeVisible()
.withTimeout(5000);
await waitFor(element(by.id('room-view-join-button')))
.toExist()
.withTimeout(2000);
let n = 0;
while (n < 3) {
try {
await element(by.id('room-view-join-button')).tap();
await waitFor(element(by.id('join-code')))
.toBeVisible()
.withTimeout(500);
} catch (error) {
n += 1;
}
}
}
describe('Join protected room', () => {

View File

@ -10,7 +10,7 @@ async function navigateToRoom(search) {
.withTimeout(10000);
await sleep(300); // app takes some time to animate
await element(by.id(`directory-view-item-${search}`)).tap();
await waitFor(element(by.id('room-view')))
await waitFor(element(by.id('room-view')).atIndex(0))
.toExist()
.withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${search}`)))
@ -44,20 +44,20 @@ describe('Join room from directory', () => {
.toExist()
.withTimeout(2000);
await element(by.id('directory-view-dropdown')).tap();
await element(by.label('Users')).tap();
await element(by.label('Search by')).tap();
await element(by.label('Users')).atIndex(0).tap();
await element(by.label('Search by')).atIndex(0).tap();
await navigateToRoom(data.users.alternate.username);
});
it('should search user and navigate', async () => {
it('should search team and navigate', async () => {
await tapBack();
await element(by.id('rooms-list-view-directory')).tap();
await waitFor(element(by.id('directory-view')))
.toExist()
.withTimeout(2000);
await element(by.id('directory-view-dropdown')).tap();
await element(by.label('Teams')).tap();
await element(by.label('Search by')).tap();
await element(by.label('Teams')).atIndex(0).tap();
await element(by.label('Search by')).atIndex(0).tap();
await navigateToRoom(data.teams.private.name);
});
});

View File

@ -1,9 +1,12 @@
const data = require('../../data');
const { sleep, navigateToLogin, login, checkServer } = require('../../helpers/app');
const { sleep, navigateToLogin, login, checkServer, platformTypes } = require('../../helpers/app');
describe('Delete server', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
@ -23,7 +26,8 @@ describe('Delete server', () => {
await waitFor(element(by.id('new-server-view')))
.toBeVisible()
.withTimeout(10000);
await element(by.id('new-server-view-input')).typeText(`${data.alternateServer}\n`);
await element(by.id('new-server-view-input')).replaceText(`${data.alternateServer}`);
await element(by.id('new-server-view-input')).tapReturnKey();
await waitFor(element(by.id('workspace-view')))
.toBeVisible()
.withTimeout(10000);
@ -36,7 +40,7 @@ describe('Delete server', () => {
await element(by.id('register-view-name')).replaceText(data.registeringUser3.username);
await element(by.id('register-view-username')).replaceText(data.registeringUser3.username);
await element(by.id('register-view-email')).replaceText(data.registeringUser3.email);
await element(by.id('register-view-password')).typeText(data.registeringUser3.password);
await element(by.id('register-view-password')).replaceText(data.registeringUser3.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
@ -51,7 +55,7 @@ describe('Delete server', () => {
.toBeVisible()
.withTimeout(5000);
await element(by.id(`rooms-list-header-server-${data.server}`)).longPress(1500);
await element(by.label('Delete').and(by.type('_UIAlertControllerActionView'))).tap();
await element(by[textMatcher]('Delete').and(by.type(alertButtonType))).tap();
await element(by.id('rooms-list-header-server-dropdown-button')).tap();
await waitFor(element(by.id('rooms-list-header-server-dropdown')))
.toBeVisible()

View File

@ -1,10 +1,13 @@
const data = require('../../data');
const { tapBack, checkServer, navigateToRegister } = require('../../helpers/app');
const { tapBack, checkServer, navigateToRegister, platformTypes } = require('../../helpers/app');
const { get, login, sendMessage } = require('../../helpers/data_setup');
const DEEPLINK_METHODS = { AUTH: 'auth', ROOM: 'room' };
let amp = '&';
const getDeepLink = (method, server, params) => {
const deeplink = `rocketchat://${method}?host=${server.replace(/^(http:\/\/|https:\/\/)/, '')}&${params}`;
const deeplink = `rocketchat://${method}?host=${server.replace(/^(http:\/\/|https:\/\/)/, '')}${amp}${params}`;
console.log(`Deeplinking to: ${deeplink}`);
return deeplink;
};
@ -12,11 +15,17 @@ const getDeepLink = (method, server, params) => {
describe('Deep linking', () => {
let userId;
let authToken;
let scrollViewType;
let threadId;
let textMatcher;
let alertButtonType;
const threadMessage = `to-thread-${data.random}`;
before(async () => {
const loginResult = await login(data.users.regular.username, data.users.regular.password);
({ userId, authToken } = loginResult);
const deviceType = device.getPlatform();
amp = deviceType === 'android' ? '\\&' : '&';
({ scrollViewType, textMatcher, alertButtonType } = platformTypes[deviceType]);
// create a thread with api
const result = await sendMessage(data.users.regular, data.groups.alternate2.name, threadMessage);
threadId = result.message._id;
@ -28,10 +37,9 @@ describe('Deep linking', () => {
await device.launchApp({
permissions: { notifications: 'YES' },
delete: true,
url: getDeepLink(DEEPLINK_METHODS.AUTH, data.server, 'userId=123&token=abc'),
sourceApp: 'com.apple.mobilesafari'
url: getDeepLink(DEEPLINK_METHODS.AUTH, data.server, `userId=123${amp}token=abc`)
});
await waitFor(element(by.text("You've been logged out by the server. Please log in again.")))
await waitFor(element(by[textMatcher]("You've been logged out by the server. Please log in again.")))
.toExist()
.withTimeout(10000); // TODO: we need to improve this message
});
@ -43,9 +51,8 @@ describe('Deep linking', () => {
url: getDeepLink(
DEEPLINK_METHODS.AUTH,
data.server,
`userId=${userId}&token=${authToken}&path=group/${data.groups.private.name}`
),
sourceApp: 'com.apple.mobilesafari'
`userId=${userId}${amp}token=${authToken}${amp}path=group/${data.groups.private.name}`
)
});
await waitFor(element(by.id(`room-view-title-${data.groups.private.name}`)))
.toExist()
@ -56,7 +63,7 @@ describe('Deep linking', () => {
.withTimeout(10000);
await checkServer(data.server);
await waitFor(element(by.id(`rooms-list-view-item-${data.groups.private.name}`)))
.toBeVisible()
.toExist()
.withTimeout(2000);
};
@ -70,7 +77,8 @@ describe('Deep linking', () => {
await element(by.id('register-view-name')).replaceText(data.registeringUser4.username);
await element(by.id('register-view-username')).replaceText(data.registeringUser4.username);
await element(by.id('register-view-email')).replaceText(data.registeringUser4.email);
await element(by.id('register-view-password')).typeText(data.registeringUser4.password);
await element(by.id('register-view-password')).replaceText(data.registeringUser4.password);
await element(by.type(scrollViewType)).atIndex(0).scrollTo('bottom');
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
@ -85,8 +93,7 @@ describe('Deep linking', () => {
await device.launchApp({
permissions: { notifications: 'YES' },
newInstance: true,
url: getDeepLink(DEEPLINK_METHODS.ROOM, data.server, `path=group/${data.groups.private.name}`),
sourceApp: 'com.apple.mobilesafari'
url: getDeepLink(DEEPLINK_METHODS.ROOM, data.server, `path=group/${data.groups.private.name}`)
});
await waitFor(element(by.id(`room-view-title-${data.groups.private.name}`)))
.toExist()
@ -97,8 +104,7 @@ describe('Deep linking', () => {
await device.launchApp({
permissions: { notifications: 'YES' },
newInstance: true,
url: getDeepLink(DEEPLINK_METHODS.ROOM, data.server, `path=group/${data.groups.alternate2.name}/thread/${threadId}`),
sourceApp: 'com.apple.mobilesafari'
url: getDeepLink(DEEPLINK_METHODS.ROOM, data.server, `path=group/${data.groups.alternate2.name}/thread/${threadId}`)
});
await waitFor(element(by.id(`room-view-title-${threadMessage}`)))
.toExist()
@ -110,8 +116,7 @@ describe('Deep linking', () => {
await device.launchApp({
permissions: { notifications: 'YES' },
newInstance: true,
url: getDeepLink(DEEPLINK_METHODS.ROOM, data.server, `rid=${roomResult.data.group._id}`),
sourceApp: 'com.apple.mobilesafari'
url: getDeepLink(DEEPLINK_METHODS.ROOM, data.server, `rid=${roomResult.data.group._id}`)
});
await waitFor(element(by.id(`room-view-title-${data.groups.private.name}`)))
.toExist()
@ -135,8 +140,7 @@ describe('Deep linking', () => {
await device.launchApp({
permissions: { notifications: 'YES' },
newInstance: true,
url: getDeepLink(DEEPLINK_METHODS.ROOM, data.server, `path=group/${data.groups.private.name}`),
sourceApp: 'com.apple.mobilesafari'
url: getDeepLink(DEEPLINK_METHODS.ROOM, data.server, `path=group/${data.groups.private.name}`)
});
await waitFor(element(by.id(`room-view-title-${data.groups.private.name}`)))
.toExist()
@ -147,8 +151,7 @@ describe('Deep linking', () => {
await device.launchApp({
permissions: { notifications: 'YES' },
newInstance: true,
url: getDeepLink(DEEPLINK_METHODS.ROOM, 'https://google.com'),
sourceApp: 'com.apple.mobilesafari'
url: getDeepLink(DEEPLINK_METHODS.ROOM, 'https://google.com')
});
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()

View File

@ -29,6 +29,9 @@ const navToLanguage = async () => {
describe('i18n', () => {
describe('OS language', () => {
it("OS set to 'en' and proper translate to 'en'", async () => {
if (device.getPlatform() === 'android') {
return; // FIXME: Passing language with launch parameters doesn't work with Android
}
await device.launchApp({
...defaultLaunchArgs,
languageAndLocale: {
@ -44,6 +47,9 @@ describe('i18n', () => {
});
it("OS set to unavailable language and fallback to 'en'", async () => {
if (device.getPlatform() === 'android') {
return; // FIXME: Passing language with launch parameters doesn't work with Android
}
await device.launchApp({
...defaultLaunchArgs,
languageAndLocale: {
@ -74,7 +80,7 @@ describe('i18n', () => {
describe('Rocket.Chat language', () => {
before(async () => {
await device.launchApp(defaultLaunchArgs);
await device.launchApp({ ...defaultLaunchArgs, delete: true });
await navigateToLogin();
await login(testuser.username, testuser.password);
});
@ -113,7 +119,7 @@ describe('i18n', () => {
it("should set unsupported language and fallback to 'en'", async () => {
await post('users.setPreferences', { data: { language: 'eo' } }); // Set language to Esperanto
await device.launchApp(defaultLaunchArgs);
await device.launchApp({ ...defaultLaunchArgs, newInstance: true });
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
.withTimeout(10000);

View File

@ -1,4 +1,4 @@
const { login, navigateToLogin } = require('../../helpers/app');
const { login, navigateToLogin, sleep } = require('../../helpers/app');
const data = require('../../data');
const goToDisplayPref = async () => {
@ -14,7 +14,7 @@ const goToRoomList = async () => {
await element(by.id('sidebar-chats')).tap();
};
describe('Rooms list screen', () => {
describe('Display prefs', () => {
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, newInstance: true, delete: true });
await navigateToLogin();
@ -89,7 +89,9 @@ describe('Rooms list screen', () => {
await expect(element(by.id('display-pref-view-avatar-switch'))).toBeVisible();
await element(by.id('display-pref-view-avatar-switch')).tap();
await goToRoomList();
await expect(element(by.id('avatar'))).not.toBeVisible();
await waitFor(element(by.id('avatar').withAncestor(by.id('rooms-list-view-item-general'))))
.not.toBeVisible()
.withTimeout(2000);
});
});
});

View File

@ -3,9 +3,11 @@ const adapter = require('detox/runners/mocha/adapter');
const config = require('../../package.json').detox;
const { setup } = require('../helpers/data_setup');
const { prepareAndroid } = require('../helpers/app');
before(async () => {
await Promise.all([setup(), detox.init(config, { launchApp: false })]);
await prepareAndroid(); // Make Android less flaky
// await dataSetup()
// await detox.init(config, { launchApp: false });
// await device.launchApp({ permissions: { notifications: 'YES' } });

View File

@ -1,8 +1,12 @@
const data = require('../../data');
const { platformTypes } = require('../../helpers/app');
describe('Onboarding', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await waitFor(element(by.id('new-server-view')))
.toBeVisible()
.withTimeout(20000);
@ -19,17 +23,13 @@ describe('Onboarding', () => {
});
describe('Usage', () => {
// it('should navigate to create new workspace', async() => {
// // webviews are not supported by detox: https://github.com/wix/detox/issues/136#issuecomment-306591554
// });
it('should enter an invalid server and get error', async () => {
await element(by.id('new-server-view-input')).typeText('invalidtest\n');
const errorText = 'Oops!';
await waitFor(element(by.text(errorText)))
.toBeVisible()
.withTimeout(60000);
await element(by.text('OK')).tap();
await element(by.id('new-server-view-input')).replaceText('invalidtest');
await element(by.id('new-server-view-input')).tapReturnKey();
await waitFor(element(by[textMatcher]('Oops!')))
.toExist()
.withTimeout(10000);
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
});
it('should tap on "Join our open workspace" and navigate', async () => {
@ -44,7 +44,8 @@ describe('Onboarding', () => {
await waitFor(element(by.id('new-server-view')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('new-server-view-input')).typeText(`${data.server}\n`);
await element(by.id('new-server-view-input')).replaceText(data.server);
await element(by.id('new-server-view-input')).tapReturnKey();
await waitFor(element(by.id('workspace-view')))
.toBeVisible()
.withTimeout(60000);

View File

@ -1,9 +1,12 @@
const data = require('../../data');
const { navigateToLogin } = require('../../helpers/app');
const { navigateToLogin, platformTypes } = require('../../helpers/app');
describe('Forgot password screen', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await element(by.id('login-view-forgot-password')).tap();
await waitFor(element(by.id('forgot-password-view')))
@ -29,10 +32,10 @@ describe('Forgot password screen', () => {
it('should reset password and navigate to login', async () => {
await element(by.id('forgot-password-view-email')).replaceText(data.users.existing.email);
await element(by.id('forgot-password-view-submit')).tap();
await waitFor(element(by.text('OK')))
await waitFor(element(by[textMatcher]('OK')))
.toExist()
.withTimeout(10000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('login-view')))
.toBeVisible()
.withTimeout(60000);

View File

@ -1,9 +1,12 @@
const { navigateToRegister } = require('../../helpers/app');
const { navigateToRegister, platformTypes } = require('../../helpers/app');
const data = require('../../data');
describe('Create user screen', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToRegister();
});
@ -50,10 +53,10 @@ describe('Create user screen', () => {
await element(by.id('register-view-email')).replaceText(data.users.existing.email);
await element(by.id('register-view-password')).replaceText(data.registeringUser.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Email already exists. [403]')).atIndex(0))
await waitFor(element(by[textMatcher]('Email already exists. [403]')).atIndex(0))
.toExist()
.withTimeout(10000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
});
it('should submit username already taken and raise error', async () => {
@ -62,10 +65,10 @@ describe('Create user screen', () => {
await element(by.id('register-view-email')).replaceText(data.registeringUser.email);
await element(by.id('register-view-password')).replaceText(data.registeringUser.password);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Username is already in use')).atIndex(0))
await waitFor(element(by[textMatcher]('Username is already in use')).atIndex(0))
.toExist()
.withTimeout(10000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
});
it('should register', async () => {

View File

@ -1,9 +1,12 @@
const { navigateToLogin, tapBack } = require('../../helpers/app');
const { navigateToLogin, tapBack, platformTypes } = require('../../helpers/app');
const data = require('../../data');
describe('Login screen', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, newInstance: true, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
});
@ -58,10 +61,10 @@ describe('Login screen', () => {
await element(by.id('login-view-email')).replaceText(data.users.regular.username);
await element(by.id('login-view-password')).replaceText('NotMyActualPassword');
await element(by.id('login-view-submit')).tap();
await waitFor(element(by.text('Your credentials were rejected! Please try again.')))
await waitFor(element(by[textMatcher]('Your credentials were rejected! Please try again.')))
.toBeVisible()
.withTimeout(10000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
});
it('should login with success', async () => {

View File

@ -14,7 +14,9 @@ describe('Rooms list screen', () => {
});
it('should have room item', async () => {
await expect(element(by.id('rooms-list-view-item-general'))).toExist();
await waitFor(element(by.id('rooms-list-view-item-general')))
.toExist()
.withTimeout(10000);
});
// Render - Header

View File

@ -25,10 +25,10 @@ describe('Server history', () => {
it('should tap on a server history and navigate to login', async () => {
await element(by.id(`server-history-${data.server}`)).tap();
await waitFor(element(by.id('login-view')))
await waitFor(element(by.id('login-view-email')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.id('login-view-email'))).toHaveText(data.users.regular.username);
await expect(element(by.label(data.users.regular.username).withAncestor(by.id('login-view-email'))));
});
it('should delete server from history', async () => {

View File

@ -1,9 +1,12 @@
const data = require('../../data');
const { tapBack, navigateToLogin, login, tryTapping } = require('../../helpers/app');
const { tapBack, navigateToLogin, login, tryTapping, platformTypes } = require('../../helpers/app');
describe('Create room screen', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
@ -121,20 +124,26 @@ describe('Create room screen', () => {
describe('Usage', () => {
it('should get invalid room', async () => {
await element(by.id('create-channel-name')).typeText('general');
await element(by.id('create-channel-name')).replaceText('general');
await waitFor(element(by.id('create-channel-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.text('A channel with name general exists')))
await waitFor(element(by[textMatcher]('A channel with name general exists')))
.toExist()
.withTimeout(60000);
await expect(element(by.text('A channel with name general exists'))).toExist();
await element(by.text('OK')).tap();
await expect(element(by[textMatcher]('A channel with name general exists'))).toExist();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
});
it('should create public room', async () => {
const room = `public${data.random}`;
await element(by.id('create-channel-name')).replaceText('');
await element(by.id('create-channel-name')).typeText(room);
await element(by.id('create-channel-name')).replaceText(room);
await element(by.id('create-channel-type')).tap();
await waitFor(element(by.id('create-channel-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view')))
.toExist()
@ -175,7 +184,10 @@ describe('Create room screen', () => {
await waitFor(element(by.id('create-channel-view')))
.toExist()
.withTimeout(5000);
await element(by.id('create-channel-name')).typeText(room);
await element(by.id('create-channel-name')).replaceText(room);
await waitFor(element(by.id('create-channel-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view')))
.toExist()
@ -213,7 +225,10 @@ describe('Create room screen', () => {
await waitFor(element(by.id('create-channel-view')))
.toExist()
.withTimeout(10000);
await element(by.id('create-channel-name')).typeText(room);
await element(by.id('create-channel-name')).replaceText(room);
await waitFor(element(by.id('create-channel-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view')))
.toExist()

View File

@ -9,7 +9,8 @@ const {
starMessage,
pinMessage,
dismissReviewNag,
tryTapping
tryTapping,
platformTypes
} = require('../../helpers/app');
async function navigateToRoom(roomName) {
@ -22,9 +23,12 @@ async function navigateToRoom(roomName) {
describe('Room screen', () => {
const mainRoom = data.groups.private.name;
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
await navigateToRoom(mainRoom);
@ -79,7 +83,7 @@ describe('Room screen', () => {
describe('Messagebox', () => {
it('should send message', async () => {
await mockMessage('message');
await expect(element(by.label(`${data.random}message`)).atIndex(0)).toExist();
await expect(element(by[textMatcher](`${data.random}message`)).atIndex(0)).toExist();
});
it('should show/hide emoji keyboard', async () => {
@ -100,8 +104,8 @@ describe('Room screen', () => {
});
it('should show/hide emoji autocomplete', async () => {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText(':joy');
await sleep(300);
await waitFor(element(by.id('messagebox-container')))
.toExist()
.withTimeout(10000);
@ -112,9 +116,8 @@ describe('Room screen', () => {
});
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 element(by.id('messagebox-input')).typeText(':joy');
await sleep(300);
await waitFor(element(by.id('messagebox-container')))
.toExist()
.withTimeout(10000);
@ -124,9 +127,8 @@ describe('Room screen', () => {
});
it('should not show emoji autocomplete on semicolon in middle of a string', async () => {
await element(by.id('messagebox-input')).tap();
// await element(by.id('messagebox-input')).replaceText(':');
await element(by.id('messagebox-input')).typeText('name:is');
await sleep(300);
await waitFor(element(by.id('messagebox-container')))
.toNotExist()
.withTimeout(20000);
@ -135,8 +137,11 @@ describe('Room screen', () => {
it('should show and tap on user autocomplete and send mention', async () => {
const { username } = data.users.regular;
await element(by.id('messagebox-input')).tap();
const messageMention = `@${username}`;
const message = `${data.random}mention`;
const fullMessage = `${messageMention} ${message}`;
await element(by.id('messagebox-input')).typeText(`@${username}`);
await sleep(300);
await waitFor(element(by.id('messagebox-container')))
.toExist()
.withTimeout(4000);
@ -144,15 +149,24 @@ describe('Room screen', () => {
.toBeVisible()
.withTimeout(4000);
await tryTapping(element(by.id(`mention-item-${username}`)), 2000, true);
await expect(element(by.id('messagebox-input'))).toHaveText(`@${username} `);
await expect(element(by.id('messagebox-input'))).toHaveText(`${messageMention} `);
await tryTapping(element(by.id('messagebox-input')), 2000);
await element(by.id('messagebox-input')).typeText(`${data.random}mention`);
await element(by.id('messagebox-send-message')).tap();
// await waitFor(element(by.label(`@${ data.user } ${ data.random }mention`)).atIndex(0)).toExist().withTimeout(60000);
if (device.getPlatform() === 'ios') {
await element(by.id('messagebox-input')).typeText(message);
await element(by.id('messagebox-send-message')).tap();
const fullMessageMatcher = fullMessage.substr(1); // removes `@`
await waitFor(element(by[textMatcher](fullMessageMatcher)))
.toExist()
.withTimeout(60000);
await expect(element(by[textMatcher](fullMessageMatcher))).toExist();
await element(by[textMatcher](fullMessageMatcher)).atIndex(0).tap();
} else {
await element(by.id('messagebox-input')).replaceText(fullMessage);
await element(by.id('messagebox-send-message')).tap();
}
});
it('should not show user autocomplete on @ in the middle of a string', async () => {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText('email@gmail');
await waitFor(element(by.id('messagebox-container')))
.toNotExist()
@ -161,9 +175,7 @@ describe('Room screen', () => {
});
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'))).toExist().withTimeout(4000);
await waitFor(element(by.id('mention-item-general')))
.toBeVisible()
.withTimeout(4000);
@ -181,7 +193,6 @@ describe('Room screen', () => {
await element(by.id('messagebox-input')).clearText();
});
it('should draft message', async () => {
await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText(`${data.random}draft`);
await tapBack();
@ -197,25 +208,29 @@ describe('Room screen', () => {
describe('Message', () => {
it('should copy permalink', async () => {
await element(by.label(`${data.random}message`))
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Permalink')).atIndex(0).tap();
await element(by[textMatcher]('Permalink')).atIndex(0).tap();
// TODO: test clipboard
});
it('should copy message', async () => {
await element(by.label(`${data.random}message`))
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Copy')).atIndex(0).tap();
await element(by[textMatcher]('Copy')).atIndex(0).tap();
// TODO: test clipboard
});
@ -224,23 +239,33 @@ describe('Room screen', () => {
await starMessage('message');
await sleep(1000); // https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/2324
await element(by.label(`${data.random}message`))
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'slow', 0.5);
await waitFor(element(by.label('Unstar')).atIndex(0))
await waitFor(element(by[textMatcher]('Unstar')).atIndex(0))
.toExist()
.withTimeout(6000);
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8);
});
it('should react to message', async () => {
await element(by.label(`${data.random}message`))
await waitFor(element(by[textMatcher](`${data.random}message`)))
.toExist()
.withTimeout(60000);
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.tap();
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.id('add-reaction')).tap();
@ -258,10 +283,12 @@ describe('Room screen', () => {
});
it('should react to message with frequently used emoji', async () => {
await element(by.label(`${data.random}message`))
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await waitFor(element(by.id('message-actions-emoji-+1')))
@ -273,7 +300,7 @@ describe('Room screen', () => {
.withTimeout(60000);
});
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async () => {
it('should show reaction picker on add reaction button pressed and have frequently used emoji, and dismiss review nag', async () => {
await element(by.id('message-add-reaction')).tap();
await waitFor(element(by.id('reaction-picker')))
.toExist()
@ -291,10 +318,6 @@ describe('Room screen', () => {
.withTimeout(60000);
});
it('should ask for review', async () => {
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
});
it('should remove reaction', async () => {
await element(by.id('message-reaction-:grinning:')).tap();
await waitFor(element(by.id('message-reaction-:grinning:')))
@ -302,32 +325,43 @@ describe('Room screen', () => {
.withTimeout(60000);
});
it('should ask for review', async () => {
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
});
it('should edit message', async () => {
await mockMessage('edit');
await element(by.label(`${data.random}edit`))
await element(by[textMatcher](`${data.random}edit`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Edit')).atIndex(0).tap();
await element(by.id('messagebox-input')).typeText('ed');
await element(by[textMatcher]('Edit')).atIndex(0).tap();
await element(by.id('messagebox-input')).replaceText(`${data.random}edited`);
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.label(`${data.random}edited (edited)`)).atIndex(0))
await waitFor(element(by[textMatcher](`${data.random}edited (edited)`)).atIndex(0))
.toExist()
.withTimeout(60000);
});
it('should quote message', async () => {
await mockMessage('quote');
await element(by.label(`${data.random}quote`))
await element(by[textMatcher](`${data.random}quote`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Quote')).atIndex(0).tap();
await element(by.id('messagebox-input')).typeText(`${data.random}quoted`);
await element(by[textMatcher]('Quote')).atIndex(0).tap();
await element(by.id('messagebox-input')).replaceText(`${data.random}quoted`);
await waitFor(element(by.id('messagebox-send-message')))
.toExist()
.withTimeout(2000);
await element(by.id('messagebox-send-message')).tap();
// TODO: test if quote was sent
@ -337,13 +371,13 @@ describe('Room screen', () => {
await mockMessage('pin');
await pinMessage('pin');
await waitFor(element(by.label(`${data.random}pin`)).atIndex(0))
await waitFor(element(by[textMatcher](`${data.random}pin`)).atIndex(0))
.toExist()
.withTimeout(5000);
await waitFor(element(by.label(`${data.users.regular.username} Message pinned`)).atIndex(0))
await waitFor(element(by[textMatcher](`${data.users.regular.username} Message pinned`)).atIndex(0))
.toExist()
.withTimeout(5000);
await element(by.label(`${data.random}pin`))
await element(by[textMatcher](`${data.random}pin`))
.atIndex(0)
.longPress();
await waitFor(element(by.id('action-sheet')))
@ -351,7 +385,7 @@ describe('Room screen', () => {
.withTimeout(1000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await waitFor(element(by.label('Unpin')).atIndex(0))
await waitFor(element(by[textMatcher]('Unpin')).atIndex(0))
.toExist()
.withTimeout(2000);
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8);
@ -359,26 +393,26 @@ describe('Room screen', () => {
it('should delete message', async () => {
await mockMessage('delete');
await waitFor(element(by.label(`${data.random}delete`)).atIndex(0)).toBeVisible();
await element(by.label(`${data.random}delete`))
await waitFor(element(by[textMatcher](`${data.random}delete`)).atIndex(0)).toBeVisible();
await element(by[textMatcher](`${data.random}delete`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await waitFor(element(by.label('Delete')))
await waitFor(element(by[textMatcher]('Delete')))
.toExist()
.withTimeout(1000);
await element(by.label('Delete')).atIndex(0).tap();
await element(by[textMatcher]('Delete')).atIndex(0).tap();
const deleteAlertMessage = 'You will not be able to recover this message!';
await waitFor(element(by.text(deleteAlertMessage)).atIndex(0))
await waitFor(element(by[textMatcher](deleteAlertMessage)).atIndex(0))
.toExist()
.withTimeout(10000);
await element(by.text('Delete')).tap();
await waitFor(element(by.label(`${data.random}delete`)).atIndex(0))
await element(by[textMatcher]('Delete').and(by.type(alertButtonType))).tap();
await waitFor(element(by[textMatcher](`${data.random}delete`)).atIndex(0))
.toNotExist()
.withTimeout(2000);
});

View File

@ -1,5 +1,15 @@
const data = require('../../data');
const { navigateToLogin, login, tapBack, sleep, searchRoom, mockMessage, starMessage, pinMessage } = require('../../helpers/app');
const {
navigateToLogin,
login,
tapBack,
sleep,
searchRoom,
mockMessage,
starMessage,
pinMessage,
platformTypes
} = require('../../helpers/app');
const { sendMessage } = require('../../helpers/data_setup');
async function navigateToRoomActions(type) {
@ -43,10 +53,13 @@ async function waitForToast() {
}
describe('Room actions screen', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
});
describe('Render', () => {
@ -172,36 +185,12 @@ describe('Room actions screen', () => {
});
describe('Usage', () => {
describe('TDB', async () => {
// TODO: test into a jitsi call
// it('should NOT navigate to voice call', async() => {
// await waitFor(element(by.id('room-actions-voice'))).toExist();
// await element(by.id('room-actions-voice')).tap();
// await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000);
// await expect(element(by.id('room-actions-view'))).toExist();
// });
// TODO: test into a jitsi call
// it('should NOT navigate to video call', async() => {
// await element(by.id('room-actions-video')).tap();
// await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000);
// await expect(element(by.id('room-actions-view'))).toExist();
// });
// TODO: test share room link
// it('should NOT navigate to share room', async() => {
// await waitFor(element(by.id('room-actions-share'))).toExist();
// await element(by.id('room-actions-share')).tap();
// await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000);
// await expect(element(by.id('room-actions-view'))).toExist();
// });
});
describe('Common', () => {
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.random }mention`))).toExist().withTimeout(60000);
await backToActions();
});
@ -220,23 +209,25 @@ describe('Room actions screen', () => {
.withTimeout(5000);
// Go to starred messages
await element(by.id('room-actions-view')).swipe('up');
await waitFor(element(by.id('room-actions-starred'))).toExist();
await element(by.id('room-actions-starred')).tap();
await waitFor(element(by.id('starred-messages-view')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.label(`${data.random}messageToStar`).withAncestor(by.id('starred-messages-view'))))
await waitFor(element(by[textMatcher](`${data.random}messageToStar`).withAncestor(by.id('starred-messages-view'))))
.toExist()
.withTimeout(60000);
// Unstar message
await element(by.label(`${data.random}messageToStar`))
await element(by[textMatcher](`${data.random}messageToStar`))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.label('Unstar')).atIndex(0).tap();
await element(by[textMatcher]('Unstar')).atIndex(0).tap();
await waitFor(element(by.label(`${data.random}messageToStar`).withAncestor(by.id('starred-messages-view'))))
await waitFor(element(by[textMatcher](`${data.random}messageToStar`).withAncestor(by.id('starred-messages-view'))))
.toBeNotVisible()
.withTimeout(60000);
await backToActions();
@ -261,40 +252,22 @@ describe('Room actions screen', () => {
await waitFor(element(by.id('pinned-messages-view')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.label(`${data.random}messageToPin`).withAncestor(by.id('pinned-messages-view'))))
await waitFor(element(by[textMatcher](`${data.random}messageToPin`).withAncestor(by.id('pinned-messages-view'))))
.toExist()
.withTimeout(6000);
await element(by.label(`${data.random}messageToPin`).withAncestor(by.id('pinned-messages-view')))
await element(by[textMatcher](`${data.random}messageToPin`).withAncestor(by.id('pinned-messages-view')))
.atIndex(0)
.longPress();
await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.label('Unpin')).atIndex(0).tap();
await element(by[textMatcher]('Unpin')).atIndex(0).tap();
await waitFor(element(by.label(`${data.random}messageToPin`).withAncestor(by.id('pinned-messages-view'))))
await waitFor(element(by[textMatcher](`${data.random}messageToPin`).withAncestor(by.id('pinned-messages-view'))))
.not.toExist()
.withTimeout(6000);
await backToActions();
});
// it('should search and find a message', async() => {
// //Go back to room and send a message
// await tapBack();
// await mockMessage('messageToFind');
// //Back into Room Actions
// await element(by.id('room-header')).tap();
// await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000);
// 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'))).toExist();
// await element(by.id('search-message-view-input')).replaceText(`/${ data.random }messageToFind/`);
// await waitFor(element(by.label(`${ data.random }messageToFind`).withAncestor(by.id('search-messages-view')))).toExist().withTimeout(60000);
// await backToActions();
// });
});
describe('Notification', () => {
@ -370,14 +343,14 @@ describe('Room actions screen', () => {
.toExist()
.withTimeout(2000);
await element(by.id('room-actions-leave-channel')).tap();
await waitFor(element(by.text('Yes, leave it!')))
await waitFor(element(by[textMatcher]('Yes, leave it!')))
.toExist()
.withTimeout(2000);
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.')))
await element(by[textMatcher]('Yes, leave it!').and(by.type(alertButtonType))).tap();
await waitFor(element(by[textMatcher]('You are the last owner. Please set new owner before leaving the room.')))
.toExist()
.withTimeout(8000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('room-actions-view')))
.toExist()
.withTimeout(2000);
@ -404,6 +377,7 @@ describe('Room actions screen', () => {
.withTimeout(4000);
await element(by.id('select-users-view-search')).tap();
await element(by.id('select-users-view-search')).replaceText(user.username);
await sleep(300);
await waitFor(element(by.id(`select-users-view-item-${user.username}`)))
.toExist()
.withTimeout(10000);
@ -437,14 +411,30 @@ describe('Room actions screen', () => {
await waitFor(element(by.id(`room-members-view-item-${username}`)))
.toExist()
.withTimeout(5000);
await element(by.id(`room-members-view-item-${username}`)).tap();
await sleep(300);
await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
let n = 0;
while (n < 3) {
// Max tries three times, in case it does not register the click
try {
await element(by.id(`room-members-view-item-${username}`)).tap();
await sleep(300);
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(5000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up');
return;
} catch (e) {
n += 1;
}
}
};
const closeActionSheet = async () => {
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.6);
await waitFor(element(by.id('action-sheet')))
.toBeNotVisible()
.withTimeout(1000);
await sleep(100);
};
it('should show all users', async () => {
@ -471,11 +461,14 @@ describe('Room actions screen', () => {
it('should remove user from room', async () => {
await openActionSheet('rocket.cat');
await element(by.label('Remove from room')).atIndex(0).tap();
await waitFor(element(by.label('Are you sure?')))
await waitFor(element(by[textMatcher]('Remove from room')))
.toExist()
.withTimeout(2000);
await element(by[textMatcher]('Remove from room')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('Are you sure?')))
.toExist()
.withTimeout(5000);
await element(by.label('Yes, remove user!').and(by.type('_UIAlertControllerActionView'))).tap();
await element(by[textMatcher]('Yes, remove user!').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('room-members-view-item-rocket.cat')))
.toBeNotVisible()
.withTimeout(60000);
@ -548,24 +541,24 @@ describe('Room actions screen', () => {
it('should set/remove as mute', async () => {
await openActionSheet(user.username);
await element(by.label('Mute')).atIndex(0).tap();
await waitFor(element(by.label('Are you sure?')))
await element(by[textMatcher]('Mute')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('Are you sure?')))
.toExist()
.withTimeout(5000);
await element(by.label('Mute').and(by.type('_UIAlertControllerActionView'))).tap();
await element(by[textMatcher]('Mute').and(by.type(alertButtonType))).tap();
await waitForToast();
await openActionSheet(user.username);
await element(by.label('Unmute')).atIndex(0).tap();
await waitFor(element(by.label('Are you sure?')))
await element(by[textMatcher]('Unmute')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('Are you sure?')))
.toExist()
.withTimeout(5000);
await element(by.label('Unmute').and(by.type('_UIAlertControllerActionView'))).tap();
await element(by[textMatcher]('Unmute').and(by.type(alertButtonType))).tap();
await waitForToast();
await openActionSheet(user.username);
// Tests if Remove as mute worked
await waitFor(element(by.label('Mute')))
await waitFor(element(by[textMatcher]('Mute')))
.toExist()
.withTimeout(5000);
await closeActionSheet();
@ -576,21 +569,21 @@ describe('Room actions screen', () => {
const channelName = `#${data.groups.private.name}`;
await sendMessage(user, channelName, message);
await openActionSheet(user.username);
await element(by.label('Ignore')).atIndex(0).tap();
await element(by[textMatcher]('Ignore')).atIndex(0).tap();
await waitForToast();
await backToActions();
await tapBack();
await waitFor(element(by.id('room-view')))
.toExist()
.withTimeout(60000);
await waitFor(element(by.label('Message ignored. Tap to display it.')).atIndex(0))
await waitFor(element(by[textMatcher]('Message ignored. Tap to display it.')).atIndex(0))
.toExist()
.withTimeout(60000);
await element(by.label('Message ignored. Tap to display it.')).atIndex(0).tap();
await waitFor(element(by.label(message)).atIndex(0))
await element(by[textMatcher]('Message ignored. Tap to display it.')).atIndex(0).tap();
await waitFor(element(by[textMatcher](message)).atIndex(0))
.toExist()
.withTimeout(60000);
await element(by.label(message)).atIndex(0).tap();
await element(by[textMatcher](message)).atIndex(0).tap();
});
it('should navigate to direct message', async () => {
@ -607,7 +600,7 @@ describe('Room actions screen', () => {
.toExist()
.withTimeout(60000);
await openActionSheet(user.username);
await element(by.label('Direct message')).atIndex(0).tap();
await element(by[textMatcher]('Direct message')).atIndex(0).tap();
await waitFor(element(by.id('room-view')))
.toExist()
.withTimeout(60000);
@ -630,11 +623,11 @@ describe('Room actions screen', () => {
it('should block/unblock user', async () => {
await waitFor(element(by.id('room-actions-block-user'))).toExist();
await element(by.id('room-actions-block-user')).tap();
await waitFor(element(by.label('Unblock user')))
await waitFor(element(by[textMatcher]('Unblock user')))
.toExist()
.withTimeout(60000);
await element(by.id('room-actions-block-user')).tap();
await waitFor(element(by.label('Block user')))
await waitFor(element(by[textMatcher]('Block user')))
.toExist()
.withTimeout(60000);
});

View File

@ -1,4 +1,4 @@
const { navigateToLogin, login, mockMessage, tapBack, searchRoom } = require('../../helpers/app');
const { navigateToLogin, login, mockMessage, tapBack, searchRoom, platformTypes } = require('../../helpers/app');
const data = require('../../data');
const channel = data.groups.private.name;
@ -12,8 +12,10 @@ const navigateToRoom = async () => {
};
describe('Discussion', () => {
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, newInstance: true, delete: true });
({ textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
@ -24,12 +26,12 @@ describe('Discussion', () => {
await waitFor(element(by.id('new-message-view')))
.toExist()
.withTimeout(2000);
await element(by.label('Create Discussion')).atIndex(0).tap();
await element(by[textMatcher]('Create Discussion')).atIndex(0).tap();
await waitFor(element(by.id('create-discussion-view')))
.toExist()
.withTimeout(60000);
await expect(element(by.id('create-discussion-view'))).toExist();
await element(by.label('Select a Channel...')).tap();
await element(by[textMatcher]('Select a Channel...')).tap();
await element(by.id('multi-select-search')).replaceText(`${channel}`);
await waitFor(element(by.id(`multi-select-item-${channel}`)))
.toExist()
@ -59,7 +61,7 @@ describe('Discussion', () => {
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await element(by.label('Create Discussion')).atIndex(0).tap();
await element(by[textMatcher]('Create Discussion')).atIndex(0).tap();
await waitFor(element(by.id('create-discussion-view')))
.toExist()
.withTimeout(2000);
@ -86,11 +88,11 @@ describe('Discussion', () => {
it('should create discussion', async () => {
const discussionName = `${data.random}message`;
await element(by.label(discussionName)).atIndex(0).longPress();
await element(by[textMatcher](discussionName)).atIndex(0).longPress();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await element(by.label('Start a Discussion')).atIndex(0).tap();
await element(by[textMatcher]('Start a Discussion')).atIndex(0).tap();
await waitFor(element(by.id('create-discussion-view')))
.toExist()
.withTimeout(2000);
@ -136,6 +138,7 @@ describe('Discussion', () => {
});
it('should have starred', async () => {
await element(by.id('room-actions-scrollview')).swipe('up', 'slow', 0.5);
await expect(element(by.id('room-actions-starred'))).toBeVisible();
});

View File

@ -1,5 +1,14 @@
const data = require('../../data');
const { navigateToLogin, login, mockMessage, tapBack, sleep, searchRoom, dismissReviewNag } = require('../../helpers/app');
const {
navigateToLogin,
login,
mockMessage,
tapBack,
sleep,
searchRoom,
platformTypes,
dismissReviewNag
} = require('../../helpers/app');
async function navigateToRoom(roomName) {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
@ -7,21 +16,22 @@ async function navigateToRoom(roomName) {
await login(data.users.regular.username, data.users.regular.password);
await searchRoom(`${roomName}`);
await element(by.id(`rooms-list-view-item-${roomName}`)).tap();
await waitFor(element(by.id('room-view')))
.toBeVisible()
await waitFor(element(by.id(`room-view-title-${roomName}`)))
.toExist()
.withTimeout(5000);
}
describe('Threads', () => {
const mainRoom = data.groups.private.name;
let textMatcher;
before(async () => {
({ textMatcher } = platformTypes[device.getPlatform()]);
await navigateToRoom(mainRoom);
});
describe('Render', () => {
it('should have room screen', async () => {
await expect(element(by.id('room-view'))).toExist();
await waitFor(element(by.id(`room-view-title-${mainRoom}`)))
.toExist()
.withTimeout(5000);
@ -69,12 +79,15 @@ describe('Threads', () => {
const thread = `${data.random}thread`;
it('should create thread', async () => {
await mockMessage('thread');
await element(by.label(thread)).atIndex(0).longPress();
await element(by[textMatcher](thread)).atIndex(0).longPress();
await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Reply in Thread')).atIndex(0).tap();
await element(by.id('messagebox-input')).typeText('replied');
await element(by[textMatcher]('Reply in Thread')).atIndex(0).tap();
await element(by.id('messagebox-input')).replaceText('replied');
await waitFor(element(by.id('messagebox-send-message')))
.toExist()
.withTimeout(2000);
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.id(`message-thread-button-${thread}`)))
.toExist()
@ -84,9 +97,6 @@ describe('Threads', () => {
it('should navigate to thread from button', async () => {
await element(by.id(`message-thread-button-${thread}`)).tap();
await waitFor(element(by.id('room-view')))
.toExist()
.withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${thread}`)))
.toExist()
.withTimeout(5000);
@ -96,13 +106,9 @@ describe('Threads', () => {
it('should toggle follow thread', async () => {
await element(by.id(`message-thread-button-${thread}`)).tap();
await waitFor(element(by.id('room-view')))
.toExist()
.withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${thread}`)))
.toExist()
.withTimeout(5000);
await expect(element(by.id(`room-view-title-${thread}`))).toExist();
await element(by.id('room-view-header-unfollow')).tap();
await waitFor(element(by.id('room-view-header-follow')))
.toExist()
@ -119,14 +125,13 @@ describe('Threads', () => {
const messageText = 'threadonly';
await mockMessage(messageText, true);
await tapBack();
await waitFor(element(by.id('room-header').and(by.label(`${mainRoom}`))))
.toBeVisible()
.withTimeout(2000);
await waitFor(element(by.id('room-header').and(by.label(`${data.random}thread`))))
.toBeNotVisible()
.withTimeout(2000);
await sleep(500); // TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
await waitFor(element(by.label(`${data.random}${messageText}`)).atIndex(0))
await waitFor(element(by.id(`room-view-title-${data.random}thread`)))
.not.toExist()
.withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${mainRoom}`)))
.toExist()
.withTimeout(5000);
await waitFor(element(by[textMatcher](`${data.random}${messageText}`)).atIndex(0))
.toNotExist()
.withTimeout(2000);
});
@ -137,40 +142,39 @@ describe('Threads', () => {
await waitFor(element(by.id('messagebox-input-thread')))
.toExist()
.withTimeout(5000);
await element(by.id('messagebox-input-thread')).typeText(messageText);
await element(by.id('messagebox-input-thread')).replaceText(messageText);
await element(by.id('messagebox-send-to-channel')).tap();
await element(by.id('messagebox-send-message')).tap();
await tapBack();
await waitFor(element(by.id('room-header').and(by.label(`${mainRoom}`))))
.toBeVisible()
.withTimeout(2000);
await waitFor(element(by.id('room-header').and(by.label(`${data.random}thread`))))
.toBeNotVisible()
.withTimeout(2000);
await sleep(500); // TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
await waitFor(element(by.label(messageText)).atIndex(0))
await waitFor(element(by.id(`room-view-title-${data.random}thread`)))
.not.toExist()
.withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${mainRoom}`)))
.toExist()
.withTimeout(5000);
await waitFor(element(by[textMatcher](messageText)).atIndex(0))
.toExist()
.withTimeout(2000);
});
it('should navigate to thread from thread name', async () => {
const messageText = 'navthreadname';
await mockMessage('dummymessagebetweenthethread');
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
await mockMessage('dummymessagebetweenthethread'); // TODO: Create a proper test for this elsewhere.
await dismissReviewNag();
await element(by.id(`message-thread-button-${thread}`)).tap();
await waitFor(element(by.id('messagebox-input-thread')))
.toExist()
.withTimeout(5000);
await element(by.id('messagebox-input-thread')).typeText(messageText);
await element(by.id('messagebox-input-thread')).replaceText(messageText);
await element(by.id('messagebox-send-to-channel')).tap();
await element(by.id('messagebox-send-message')).tap();
await tapBack();
await waitFor(element(by.id('room-header').and(by.label(`${mainRoom}`))))
.toBeVisible()
.withTimeout(2000);
await waitFor(element(by.id('room-header').and(by.label(`${data.random}thread`))))
.toBeNotVisible()
.withTimeout(2000);
await waitFor(element(by.id(`room-view-title-${data.random}thread`)))
.not.toExist()
.withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${mainRoom}`)))
.toExist()
.withTimeout(5000);
await waitFor(element(by.id(`message-thread-replied-on-${thread}`)))
.toBeVisible()
.withTimeout(2000);
@ -211,7 +215,7 @@ describe('Threads', () => {
await waitFor(element(by.id(`room-view-title-${thread}`)))
.toExist()
.withTimeout(5000);
await element(by.id('messagebox-input-thread')).typeText(`${thread}draft`);
await element(by.id('messagebox-input-thread')).replaceText(`${thread}draft`);
await tapBack();
await element(by.id(`message-thread-button-${thread}`)).tap();

View File

@ -1,9 +1,11 @@
const data = require('../../data');
const { navigateToLogin, login } = require('../../helpers/app');
const { navigateToLogin, login, platformTypes } = require('../../helpers/app');
describe('Group DM', () => {
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
@ -29,7 +31,7 @@ describe('Group DM', () => {
describe('Usage', () => {
it('should navigate to create DM', async () => {
await element(by.label('Create Direct Messages')).tap();
await element(by[textMatcher]('Create Direct Messages')).tap();
});
it('should add users', async () => {

View File

@ -1,5 +1,5 @@
const data = require('../../data');
const { navigateToLogin, login, searchRoom, sleep } = require('../../helpers/app');
const { navigateToLogin, login, searchRoom, sleep, platformTypes } = require('../../helpers/app');
const { sendMessage } = require('../../helpers/data_setup');
async function navigateToRoom(user) {
@ -12,9 +12,11 @@ async function navigateToRoom(user) {
describe('Mark as unread', () => {
const user = data.users.alternate.username;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
await navigateToRoom(user);
@ -26,16 +28,16 @@ describe('Mark as unread', () => {
const message = `${data.random}message-mark-as-unread`;
const channelName = `@${data.users.regular.username}`;
await sendMessage(data.users.alternate, channelName, message);
await waitFor(element(by.label(message)).atIndex(0))
await waitFor(element(by[textMatcher](message)).atIndex(0))
.toExist()
.withTimeout(30000);
await sleep(300);
await element(by.label(message)).atIndex(0).longPress();
await element(by[textMatcher](message)).atIndex(0).longPress();
await waitFor(element(by.id('action-sheet-handle')))
.toBeVisible()
.withTimeout(3000);
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Mark Unread')).atIndex(0).tap();
await element(by[textMatcher]('Mark Unread')).atIndex(0).tap();
await waitFor(element(by.id('rooms-list-view')))
.toExist()
.withTimeout(5000);

View File

@ -1,5 +1,5 @@
const data = require('../../data');
const { navigateToLogin, login, tapBack, sleep, searchRoom } = require('../../helpers/app');
const { navigateToLogin, login, tapBack, sleep, searchRoom, platformTypes } = require('../../helpers/app');
const privateRoomName = data.groups.private.name;
@ -25,17 +25,20 @@ async function navigateToRoomInfo(type) {
.withTimeout(2000);
}
async function swipe(direction) {
await element(by.id('room-info-edit-view-list')).swipe(direction, 'fast', 0.8);
}
async function waitForToast() {
// await waitFor(element(by.id('toast'))).toExist().withTimeout(10000);
// await expect(element(by.id('toast'))).toExist();
// await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000);
// await expect(element(by.id('toast'))).toBeNotVisible();
await sleep(300);
}
describe('Room info screen', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
@ -72,15 +75,15 @@ describe('Room info screen', () => {
});
it('should have description', async () => {
await expect(element(by.label('Description'))).toExist();
await expect(element(by[textMatcher]('Description'))).toExist();
});
it('should have topic', async () => {
await expect(element(by.label('Topic'))).toExist();
await expect(element(by[textMatcher]('Topic'))).toExist();
});
it('should have announcement', async () => {
await expect(element(by.label('Announcement'))).toExist();
await expect(element(by[textMatcher]('Announcement'))).toExist();
});
it('should have edit button', async () => {
@ -124,8 +127,7 @@ describe('Room info screen', () => {
});
it('should have type switch', async () => {
// Ugly hack to scroll on detox
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.8);
await swipe('up');
await expect(element(by.id('room-info-edit-view-t'))).toExist();
});
@ -150,44 +152,33 @@ describe('Room info screen', () => {
});
after(async () => {
// Ugly hack to scroll on detox
await element(by.id('room-info-edit-view-list')).swipe('down', 'fast', 0.8);
await swipe('down');
});
});
describe('Usage', () => {
// it('should enter "invalid name" and get error', async() => {
// await element(by.type('UIScrollView')).atIndex(1).swipe('down');
// await element(by.id('room-info-edit-view-name')).replaceText('invalid name');
// await element(by.type('UIScrollView')).atIndex(1).swipe('up');
// await element(by.id('room-info-edit-view-submit')).tap();
// await waitFor(element(by.text('There was an error while saving settings!'))).toExist().withTimeout(60000);
// await expect(element(by.text('There was an error while saving settings!'))).toExist();
// await element(by.text('OK')).tap();
// await waitFor(element(by.text('There was an error while saving settings!'))).toBeNotVisible().withTimeout(10000);
// await element(by.type('UIScrollView')).atIndex(1).swipe('down');
// });
it('should change room name', async () => {
await element(by.id('room-info-edit-view-name')).replaceText(`${privateRoomName}new`);
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
await tapBack();
await waitFor(element(by.id('room-info-view')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('room-info-view-name'))).toHaveLabel(`${privateRoomName}new`);
const matcher = device.getPlatform() === 'android' ? 'toHaveText' : 'toHaveLabel';
await expect(element(by.id('room-info-view-name')))[matcher](`${privateRoomName}new`);
// change name to original
await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view')))
.toExist()
.withTimeout(2000);
await element(by.id('room-info-edit-view-name')).replaceText(`${privateRoomName}`);
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
await element(by.id('room-info-edit-view-list')).swipe('down', 'fast', 0.8);
await swipe('down');
});
it('should reset form', async () => {
@ -196,10 +187,11 @@ describe('Room info screen', () => {
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', 'fast', 0.5);
await element(by.id('room-info-edit-view-t')).tap();
await swipe('up');
await element(by.id('room-info-edit-view-ro')).longPress(); // https://github.com/facebook/react-native/issues/28032
await element(by.id('room-info-edit-view-react-when-ro')).tap();
await swipe('up');
await element(by.id('room-info-edit-view-reset')).tap();
// after reset
await expect(element(by.id('room-info-edit-view-name'))).toHaveText(privateRoomName);
@ -207,22 +199,23 @@ describe('Room info screen', () => {
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 swipe('down');
await expect(element(by.id('room-info-edit-view-t'))).toHaveToggleValue(true);
await expect(element(by.id('room-info-edit-view-ro'))).toHaveToggleValue(false);
await expect(element(by.id('room-info-edit-view-react-when-ro'))).toBeNotVisible();
await element(by.id('room-info-edit-view-list')).swipe('down', 'fast', 0.8);
await 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', 'fast', 0.5);
await swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
await tapBack();
await waitFor(element(by.id('room-info-view')))
.toExist()
.withTimeout(2000);
await expect(element(by.label('new description').withAncestor(by.id('room-info-view-description')))).toExist();
await expect(element(by[textMatcher]('new description').withAncestor(by.id('room-info-view-description')))).toExist();
});
it('should change room topic', async () => {
@ -234,14 +227,14 @@ describe('Room info screen', () => {
.toExist()
.withTimeout(2000);
await element(by.id('room-info-edit-view-topic')).replaceText('new topic');
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
await tapBack();
await waitFor(element(by.id('room-info-view')))
.toExist()
.withTimeout(2000);
await expect(element(by.label('new topic').withAncestor(by.id('room-info-view-topic')))).toExist();
await expect(element(by[textMatcher]('new topic').withAncestor(by.id('room-info-view-topic')))).toExist();
});
it('should change room announcement', async () => {
@ -253,14 +246,14 @@ describe('Room info screen', () => {
.toExist()
.withTimeout(2000);
await element(by.id('room-info-edit-view-announcement')).replaceText('new announcement');
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
await tapBack();
await waitFor(element(by.id('room-info-view')))
.toExist()
.withTimeout(2000);
await expect(element(by.label('new announcement').withAncestor(by.id('room-info-view-announcement')))).toExist();
await expect(element(by[textMatcher]('new announcement').withAncestor(by.id('room-info-view-announcement')))).toExist();
});
it('should change room password', async () => {
@ -271,61 +264,45 @@ describe('Room info screen', () => {
await waitFor(element(by.id('room-info-edit-view')))
.toExist()
.withTimeout(2000);
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await element(by.id('room-info-edit-view-password')).replaceText('password');
await swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
});
it('should change room type', async () => {
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await swipe('down');
await element(by.id('room-info-edit-view-t')).tap();
await swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
await swipe('down');
await element(by.id('room-info-edit-view-t')).tap();
await swipe('up');
await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast();
});
// it('should change room read only and allow reactions', async() => {
// await sleep(1000);
// await element(by.type('UIScrollView')).atIndex(1).swipe('up');
// await element(by.id('room-info-edit-view-ro')).tap();
// await waitFor(element(by.id('room-info-edit-view-react-when-ro'))).toExist().withTimeout(2000);
// await expect(element(by.id('room-info-edit-view-react-when-ro'))).toExist();
// await element(by.id('room-info-edit-view-react-when-ro')).tap();
// await element(by.id('room-info-edit-view-submit')).tap();
// await waitForToast();
// // TODO: test if it's possible to react
// });
it('should archive room', async () => {
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await element(by.id('room-info-edit-view-archive')).tap();
await waitFor(element(by.text('Yes, archive it!')))
await waitFor(element(by[textMatcher]('Yes, archive it!')))
.toExist()
.withTimeout(5000);
await element(by.text('Yes, archive it!')).tap();
await element(by[textMatcher]('Yes, archive it!').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('room-info-edit-view-unarchive')))
.toExist()
.withTimeout(60000);
await expect(element(by.id('room-info-edit-view-archive'))).toBeNotVisible();
// TODO: needs permission to unarchive
// await element(by.id('room-info-edit-view-archive')).tap();
// await waitFor(element(by.text('Yes, unarchive it!'))).toExist().withTimeout(5000);
// await expect(element(by.text('Yes, unarchive it!'))).toExist();
// await element(by.text('Yes, unarchive it!')).tap();
// await waitFor(element(by.text('ARCHIVE'))).toExist().withTimeout(60000);
// await expect(element(by.text('ARCHIVE'))).toExist();
});
it('should delete room', async () => {
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await swipe('up');
await element(by.id('room-info-edit-view-delete')).tap();
await waitFor(element(by.text('Yes, delete it!')))
await waitFor(element(by[textMatcher]('Yes, delete it!')))
.toExist()
.withTimeout(5000);
await element(by.text('Yes, delete it!')).tap();
await element(by[textMatcher]('Yes, delete it!').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('rooms-list-view')))
.toExist()
.withTimeout(10000);

View File

@ -1,5 +1,5 @@
const data = require('../../data');
const { navigateToLogin, tapBack, login, searchRoom } = require('../../helpers/app');
const { navigateToLogin, tapBack, login, searchRoom, sleep, platformTypes } = require('../../helpers/app');
async function navigateToRoom(roomName) {
await searchRoom(`${roomName}`);
@ -7,8 +7,14 @@ async function navigateToRoom(roomName) {
await waitFor(element(by.id('room-view')))
.toBeVisible()
.withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${roomName}`)))
.toExist()
.withTimeout(5000);
}
let textMatcher;
let alertButtonType;
async function clearCache() {
await waitFor(element(by.id('room-view')))
.toBeVisible()
@ -26,10 +32,10 @@ async function clearCache() {
.toBeVisible()
.withTimeout(2000);
await element(by.id('settings-view-clear-cache')).tap();
await waitFor(element(by.text('This will clear all your offline data.')))
await waitFor(element(by[textMatcher]('This will clear all your offline data.')))
.toExist()
.withTimeout(2000);
await element(by.label('Clear').and(by.type('_UIAlertControllerActionView'))).tap();
await element(by[textMatcher]('Clear').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('rooms-list-view')))
.toBeVisible()
.withTimeout(5000);
@ -39,6 +45,10 @@ async function clearCache() {
}
async function waitForLoading() {
if (device.getPlatform() === 'android') {
await sleep(10000);
return; // FIXME: Loading indicator doesn't animate properly on android
}
await waitFor(element(by.id('loading')))
.toBeVisible()
.withTimeout(5000);
@ -50,21 +60,22 @@ async function waitForLoading() {
describe('Room', () => {
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.adminUser, data.adminPassword);
});
it('should jump to an old message and load its surroundings', async () => {
await navigateToRoom('jumping');
await waitFor(element(by.label('Quote first message')))
await waitFor(element(by[textMatcher]('300')))
.toExist()
.withTimeout(5000);
await element(by.label('1')).atIndex(0).tap();
await element(by[textMatcher]('1')).atIndex(0).tap();
await waitForLoading();
await waitFor(element(by.label('1')).atIndex(0))
await waitFor(element(by[textMatcher]('1')).atIndex(0))
.toExist()
.withTimeout(10000);
await expect(element(by.label('2'))).toExist();
await expect(element(by[textMatcher]('2'))).toExist();
});
it('should tap FAB and scroll to bottom', async () => {
@ -72,7 +83,7 @@ describe('Room', () => {
.toExist()
.withTimeout(5000);
await element(by.id('nav-jump-to-bottom')).tap();
await waitFor(element(by.label('Quote first message')))
await waitFor(element(by[textMatcher]('Quote first message')))
.toExist()
.withTimeout(5000);
await clearCache();
@ -83,14 +94,15 @@ describe('Room', () => {
await waitFor(element(by.id('room-view-messages')))
.toExist()
.withTimeout(5000);
await waitFor(element(by.label('300')))
await waitFor(element(by[textMatcher]('300')))
.toExist()
.withTimeout(5000);
let found = false;
while (!found) {
await element(by.id('room-view-messages')).atIndex(0).scroll(500, 'up');
try {
await expect(element(by.label('249'))).toExist();
const direction = device.getPlatform() === 'android' ? 'down' : 'up';
await element(by.id('room-view-messages')).scroll(500, direction);
await expect(element(by[textMatcher]('249'))).toExist();
found = true;
} catch {
//
@ -101,107 +113,130 @@ describe('Room', () => {
it('should search for old message and load its surroundings', async () => {
await navigateToRoom('jumping');
await sleep(1000); // wait for proper load the room
await element(by.id('room-view-search')).tap();
await waitFor(element(by.id('search-messages-view')))
.toExist()
.withTimeout(5000);
await element(by.id('search-message-view-input')).typeText('30\n');
await waitFor(element(by.label('30')).atIndex(0))
await element(by.id('search-message-view-input')).replaceText('30');
await waitFor(element(by[textMatcher]('30')).atIndex(1))
.toExist()
.withTimeout(5000);
await element(by.label('30')).atIndex(0).tap();
.withTimeout(30000);
await element(by[textMatcher]('30')).atIndex(1).tap();
await waitForLoading();
await expect(element(by.label('30'))).toExist();
await expect(element(by.label('31'))).toExist();
await expect(element(by.label('32'))).toExist();
await waitFor(element(by[textMatcher]('30')).atIndex(0))
.toExist()
.withTimeout(30000);
await expect(element(by[textMatcher]('31'))).toExist();
await expect(element(by[textMatcher]('32'))).toExist();
});
it('should load newer and older messages', async () => {
await element(by.id('room-view-messages')).atIndex(0).swipe('down', 'fast', 0.8);
await waitFor(element(by.label('5')))
.toExist()
.withTimeout(10000);
await waitFor(element(by.label('Load Older')))
.toExist()
.withTimeout(5000);
await element(by.label('Load Older')).atIndex(0).tap();
await waitFor(element(by.label('4')))
// TODO: couldn't make it work on Android :(
if (device.getPlatform() === 'android') {
return;
}
let found = false;
while (!found) {
try {
// it doesn't recognize this list
await element(by.id('room-view-messages')).scroll(500, 'up');
await expect(element(by[textMatcher]('Load Older'))).toBeVisible();
await expect(element(by[textMatcher]('5'))).toExist();
found = true;
} catch {
//
}
}
await element(by[textMatcher]('Load Older')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('4')))
.toExist()
.withTimeout(5000);
await element(by.id('room-view-messages')).atIndex(0).swipe('down', 'fast', 0.5);
await waitFor(element(by.label('1')))
await waitFor(element(by[textMatcher]('1')))
.toExist()
.withTimeout(5000);
await element(by.id('room-view-messages')).atIndex(0).swipe('up', 'fast', 0.5);
await waitFor(element(by.label('25')))
await waitFor(element(by[textMatcher]('25')))
.toExist()
.withTimeout(5000);
await element(by.id('room-view-messages')).atIndex(0).swipe('up', 'fast', 0.5);
await waitFor(element(by.label('50')))
await waitFor(element(by[textMatcher]('50')))
.toExist()
.withTimeout(5000);
await element(by.id('room-view-messages')).atIndex(0).swipe('up', 'slow', 0.5);
await waitFor(element(by.label('Load Newer')))
await waitFor(element(by[textMatcher]('Load Newer')))
.toExist()
.withTimeout(5000);
await element(by.label('Load Newer')).atIndex(0).tap();
await waitFor(element(by.label('104')))
await element(by[textMatcher]('Load Newer')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('104')))
.toExist()
.withTimeout(5000);
await waitFor(element(by.label('Load Newer')))
await waitFor(element(by[textMatcher]('Load Newer')))
.toExist()
.withTimeout(5000);
await element(by.label('Load Newer')).atIndex(0).tap();
await waitFor(element(by.label('154')))
await element(by[textMatcher]('Load Newer')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('154')))
.toExist()
.withTimeout(5000);
await waitFor(element(by.label('Load Newer')))
await waitFor(element(by[textMatcher]('Load Newer')))
.toExist()
.withTimeout(5000);
await element(by.label('Load Newer')).atIndex(0).tap();
await waitFor(element(by.label('Load Newer')))
await element(by[textMatcher]('Load Newer')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('Load Newer')))
.toNotExist()
.withTimeout(5000);
await expect(element(by.label('Load More'))).toNotExist();
await expect(element(by.label('201'))).toExist();
await expect(element(by.label('202'))).toExist();
await expect(element(by[textMatcher]('Load More'))).toNotExist();
await expect(element(by[textMatcher]('201'))).toExist();
await expect(element(by[textMatcher]('202'))).toExist();
await tapBack();
});
});
const expectThreadMessages = async message => {
await waitFor(element(by.id('room-view-title-jumping-thread')))
await waitFor(element(by.id('room-view-title-thread 1')))
.toExist()
.withTimeout(5000);
await expect(element(by.label(message))).toExist();
await waitForLoading();
await expect(element(by[textMatcher](message)).atIndex(0)).toExist();
await element(by[textMatcher](message)).atIndex(0).tap();
};
describe('Threads', () => {
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, newInstance: true });
});
it('should navigate to a thread from another room', async () => {
await navigateToRoom('jumping');
await waitFor(element(by.label("Go to jumping-thread's thread")).atIndex(0))
await waitFor(element(by[textMatcher]("Go to jumping-thread's thread")).atIndex(0))
.toExist()
.withTimeout(5000);
await element(by.label("Go to jumping-thread's thread")).atIndex(0).tap();
await waitForLoading();
await element(by[textMatcher]("Go to jumping-thread's thread")).atIndex(0).tap();
await expectThreadMessages("Go to jumping-thread's thread");
await tapBack();
});
it('should tap on thread message from main room', async () => {
await waitFor(element(by.label('thread message sent to main room')).atIndex(0))
await waitFor(element(by.id('room-view-title-jumping-thread')))
.toExist()
.withTimeout(5000);
await element(by.label('thread message sent to main room')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('thread message sent to main room')))
.toExist()
.withTimeout(10000);
await element(by[textMatcher]('thread message sent to main room')).atIndex(0).tap();
await expectThreadMessages('thread message sent to main room');
await tapBack();
});
it('should tap on quote', async () => {
await waitFor(element(by.label('quoted')))
await waitFor(element(by.id('room-view-title-jumping-thread')))
.toExist()
.withTimeout(5000);
await element(by.label('quoted')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('quoted')))
.toExist()
.withTimeout(5000);
await element(by[textMatcher]('quoted')).atIndex(0).tap();
await expectThreadMessages('quoted');
await tapBack();
});
@ -214,11 +249,11 @@ describe('Threads', () => {
await waitFor(element(by.id('search-messages-view')))
.toExist()
.withTimeout(5000);
await element(by.id('search-message-view-input')).typeText('to be searched\n');
await waitFor(element(by.label('to be searched')))
await element(by.id('search-message-view-input')).replaceText('to be searched');
await waitFor(element(by[textMatcher]('to be searched')).atIndex(1))
.toExist()
.withTimeout(5000);
await element(by.label('to be searched')).atIndex(1).tap();
.withTimeout(30000);
await element(by[textMatcher]('to be searched')).atIndex(1).tap();
await expectThreadMessages('to be searched');
});

View File

@ -1,11 +1,14 @@
const data = require('../../data');
const { navigateToLogin, login } = require('../../helpers/app');
const { navigateToLogin, login, platformTypes } = require('../../helpers/app');
const teamName = `team-${data.random}`;
describe('Create team screen', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
@ -41,17 +44,23 @@ describe('Create team screen', () => {
describe('Create Team', () => {
describe('Usage', () => {
it('should get invalid team name', async () => {
await element(by.id('create-channel-name')).typeText(`${data.teams.private.name}`);
await element(by.id('create-channel-name')).replaceText(`${data.teams.private.name}`);
await waitFor(element(by.id('create-channel-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.text('OK')))
await waitFor(element(by[textMatcher]('OK').and(by.type(alertButtonType))))
.toBeVisible()
.withTimeout(5000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
});
it('should create private team', async () => {
await element(by.id('create-channel-name')).replaceText('');
await element(by.id('create-channel-name')).typeText(teamName);
await element(by.id('create-channel-name')).replaceText(teamName);
await waitFor(element(by.id('create-channel-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view')))
.toExist()
@ -81,10 +90,10 @@ describe('Create team screen', () => {
await element(by.id('room-info-view-edit-button')).tap();
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
await element(by.id('room-info-edit-view-delete')).tap();
await waitFor(element(by.text('Yes, delete it!')))
await waitFor(element(by[textMatcher]('Yes, delete it!')))
.toExist()
.withTimeout(5000);
await element(by.text('Yes, delete it!')).tap();
await element(by[textMatcher]('Yes, delete it!').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('rooms-list-view')))
.toExist()
.withTimeout(10000);

View File

@ -1,5 +1,5 @@
const data = require('../../data');
const { navigateToLogin, login, tapBack, sleep, searchRoom } = require('../../helpers/app');
const { navigateToLogin, login, tapBack, sleep, searchRoom, platformTypes } = require('../../helpers/app');
async function navigateToRoom(roomName) {
await searchRoom(`${roomName}`);
@ -17,6 +17,7 @@ async function openActionSheet(username) {
await sleep(300);
await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up');
}
async function navigateToRoomActions() {
@ -37,20 +38,41 @@ async function backToActions() {
}
async function closeActionSheet() {
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.6);
await waitFor(element(by.id('action-sheet-handle')))
.toBeNotVisible()
.withTimeout(3000);
await sleep(200);
}
async function waitForToast() {
await sleep(1000);
}
async function swipeTillVisible(container, find, direction = 'up', delta = 0.3, speed = 'slow') {
let found = false;
while (!found) {
try {
await element(container).swipe(direction, speed, delta);
await sleep(200);
await expect(element(find)).toBeVisible();
found = true;
} catch (e) {
//
}
}
}
describe('Team', () => {
const team = data.teams.private.name;
const user = data.users.alternate;
const room = `private${data.random}-channel-team`;
const existingRoom = data.groups.alternate.name;
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
await navigateToRoom(team);
@ -86,7 +108,7 @@ describe('Team', () => {
describe('Team Channels Header', () => {
it('should have actions button ', async () => {
await expect(element(by.id('room-header'))).toExist();
await expect(element(by.id('room-header')).atIndex(0)).toExist();
});
it('should have team channels button ', async () => {
@ -124,6 +146,9 @@ describe('Team', () => {
await element(by.id('add-channel-team-view-create-channel')).tap();
await element(by.id('select-users-view-search')).replaceText('rocket.cat');
await waitFor(element(by.id('select-users-view-item-rocket.cat')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('select-users-view-item-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat')))
.toBeVisible()
@ -134,7 +159,10 @@ describe('Team', () => {
.toExist()
.withTimeout(10000);
await element(by.id('create-channel-name')).replaceText('');
await element(by.id('create-channel-name')).typeText(room);
await element(by.id('create-channel-name')).replaceText(room);
await waitFor(element(by.id('create-channel-submit')))
.toExist()
.withTimeout(10000);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view')))
@ -156,9 +184,9 @@ describe('Team', () => {
.toExist()
.withTimeout(60000);
await expect(element(by.id(`room-view-title-${room}`))).toExist();
await expect(element(by.id('room-view-header-team-channels'))).toExist();
await expect(element(by.id('room-view-header-threads'))).toExist();
await expect(element(by.id('room-view-search'))).toExist();
await expect(element(by.id('room-view-header-team-channels')).atIndex(0)).toExist();
await expect(element(by.id('room-view-header-threads')).atIndex(0)).toExist();
await expect(element(by.id('room-view-search')).atIndex(0)).toExist();
await tapBack();
});
@ -186,7 +214,7 @@ describe('Team', () => {
await expect(element(by.id('room-view-header-team-channels'))).toExist();
await element(by.id('room-view-header-team-channels')).tap();
await waitFor(element(by.id(`rooms-list-view-item-${existingRoom}`)))
await waitFor(element(by.id(`rooms-list-view-item-${existingRoom}`)).atIndex(0))
.toExist()
.withTimeout(10000);
});
@ -195,7 +223,8 @@ describe('Team', () => {
await element(by.id(`rooms-list-view-item-${existingRoom}`))
.atIndex(0)
.longPress();
await sleep(500);
await swipeTillVisible(by.id('action-sheet-remove-from-team'), by.id('action-sheet-delete'));
await waitFor(element(by.id('action-sheet-auto-join')))
.toBeVisible()
.withTimeout(5000);
@ -224,7 +253,7 @@ describe('Team', () => {
await waitFor(element(by.id('auto-join-tag')))
.toBeNotVisible()
.withTimeout(5000);
await waitFor(element(by.id(`rooms-list-view-item-${existingRoom}`)))
await waitFor(element(by.id(`rooms-list-view-item-${existingRoom}`)).atIndex(0))
.toExist()
.withTimeout(6000);
});
@ -298,22 +327,22 @@ describe('Team', () => {
await waitFor(
element(
by.label(
by[textMatcher](
'You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.'
)
)
)
.toExist()
.withTimeout(2000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('select-list-view-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('select-list-view-submit')).tap();
await waitFor(element(by.text('Last owner cannot be removed')))
await waitFor(element(by[textMatcher]('Last owner cannot be removed')))
.toExist()
.withTimeout(8000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
await tapBack();
await waitFor(element(by.id('room-actions-view')))
.toExist()
@ -352,6 +381,9 @@ describe('Team', () => {
it('should remove member from team', async () => {
await openActionSheet('rocket.cat');
await waitFor(element(by.id('action-sheet-remove-from-team')))
.toBeVisible()
.withTimeout(2000);
await element(by.id('action-sheet-remove-from-team')).tap();
await waitFor(element(by.id('select-list-view')))
.toExist()
@ -406,14 +438,14 @@ describe('Team', () => {
await waitFor(
element(
by.label(
by[textMatcher](
'You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.'
)
)
)
.toExist()
.withTimeout(2000);
await element(by.text('OK')).tap();
await element(by[textMatcher]('OK').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('select-list-view-submit')))
.toExist()
.withTimeout(2000);

View File

@ -1,5 +1,5 @@
const data = require('../../data');
const { navigateToLogin, login, tapBack, searchRoom, sleep } = require('../../helpers/app');
const { navigateToLogin, login, tapBack, searchRoom, sleep, platformTypes } = require('../../helpers/app');
const toBeConverted = `to-be-converted-${data.random}`;
const toBeMoved = `to-be-moved-${data.random}`;
@ -17,7 +17,10 @@ const createChannel = async room => {
await waitFor(element(by.id('create-channel-view')))
.toExist()
.withTimeout(10000);
await element(by.id('create-channel-name')).typeText(room);
await element(by.id('create-channel-name')).replaceText(room);
await waitFor(element(by.id('create-channel-submit')))
.toExist()
.withTimeout(10000);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view')))
.toExist()
@ -51,8 +54,11 @@ async function navigateToRoomActions(room) {
}
describe('Move/Convert Team', () => {
let alertButtonType;
let textMatcher;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
({ alertButtonType, textMatcher } = platformTypes[device.getPlatform()]);
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
@ -69,10 +75,10 @@ describe('Move/Convert Team', () => {
.toExist()
.withTimeout(2000);
await element(by.id('room-actions-convert-to-team')).tap();
await waitFor(element(by.label('You are converting this Channel to a Team. All Members will be kept.')))
await waitFor(element(by[textMatcher]('You are converting this Channel to a Team. All Members will be kept.')))
.toExist()
.withTimeout(2000);
await element(by.text('Convert')).tap();
await element(by[textMatcher]('Convert').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('room-view')))
.toExist()
.withTimeout(20000);
@ -101,12 +107,14 @@ describe('Move/Convert Team', () => {
.toExist()
.withTimeout(2000);
await element(by.id('room-actions-move-to-team')).tap();
await waitFor(element(by.id('select-list-view')))
await waitFor(element(by[textMatcher]('Move to Team')).atIndex(0))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id('select-list-view-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('select-list-view-submit')).tap();
await sleep(2000);
await waitFor(element(by.id('select-list-view')))
await waitFor(element(by[textMatcher]('Select Team')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id(`select-list-view-item-${toBeConverted}`)))
@ -116,14 +124,14 @@ describe('Move/Convert Team', () => {
await element(by.id('select-list-view-submit')).atIndex(0).tap();
await waitFor(
element(
by.label(
by[textMatcher](
'After reading the previous intructions about this behavior, do you still want to move this channel to the selected team?'
)
)
)
.toExist()
.withTimeout(2000);
await element(by.text('Yes, move it!')).tap();
await element(by[textMatcher]('Yes, move it!').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('room-view-header-team-channels')))
.toExist()
.withTimeout(10000);
@ -141,12 +149,11 @@ describe('Move/Convert Team', () => {
it('should convert a team to a channel', async () => {
await navigateToRoomActions(toBeConverted);
await element(by.id('room-actions-scrollview')).scrollTo('bottom');
await waitFor(element(by.id('room-actions-convert-channel-to-team')))
await waitFor(element(by[textMatcher]('Convert to Channel')))
.toExist()
.withTimeout(2000);
await element(by.id('room-actions-convert-channel-to-team')).tap();
await sleep(2000);
await waitFor(element(by.id('select-list-view')))
await element(by[textMatcher]('Convert to Channel')).atIndex(0).tap();
await waitFor(element(by[textMatcher]('Converting Team to Channel')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id(`select-list-view-item-${toBeMoved}`)))
@ -157,10 +164,10 @@ describe('Move/Convert Team', () => {
.toExist()
.withTimeout(2000);
await element(by.id('select-list-view-submit')).tap();
await waitFor(element(by.label('You are converting this Team to a Channel')))
await waitFor(element(by[textMatcher]('You are converting this Team to a Channel')))
.toExist()
.withTimeout(2000);
await element(by.text('Convert')).tap();
await element(by[textMatcher]('Convert').and(by.type(alertButtonType))).tap();
await waitFor(element(by.id('room-view')))
.toExist()
.withTimeout(20000);

View File

@ -239,6 +239,18 @@
}
}
}
},
"and.emu.debug": {
"device": "Pixel_API_28_AOSP",
"type": "android.emulator",
"binaryPath": "android/app/build/outputs/apk/e2ePlay/debug/app-e2e-play-debug.apk",
"build": "cd android && ./gradlew app:assembleE2ePlayDebug app:assembleE2ePlayDebugAndroidTest -DtestBuildType=debug && cd .."
},
"and.emu.release": {
"device": "Pixel_API_28_AOSP",
"type": "android.emulator",
"binaryPath": "android/app/build/outputs/apk/e2ePlay/release/app-e2e-play-release.apk",
"build": "cd android && ./gradlew app:assembleE2ePlayRelease app:assembleE2ePlayReleaseAndroidTest -DtestBuildType=release && cd .."
}
}
}