Unverified Commit 005e1ac4 authored by Diego Mello's avatar Diego Mello Committed by GitHub
Browse files

[FIX] Detox tests (#1790)

parent 3d535196
......@@ -44,7 +44,7 @@ jobs:
paths:
- ./node_modules
e2e-test:
e2e-build:
macos:
xcode: "11.2.1"
......@@ -80,11 +80,73 @@ jobs:
yarn global add detox-cli
yarn
- run:
name: Rebuild Detox framework cache
command: |
detox clean-framework-cache
detox build-framework-cache
- run:
name: Build
command: |
detox build --configuration ios.sim.release
- persist_to_workspace:
root: .
paths:
- ios/build/Build/Products/Release-iphonesimulator/RocketChatRN.app
- save_cache:
name: Save NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
paths:
- node_modules
e2e-test:
macos:
xcode: "11.2.1"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
steps:
- checkout
- attach_workspace:
at: .
- restore_cache:
name: Restore NPM cache
key: node-v1-mac-{{ checksum "yarn.lock" }}
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- run:
name: Install appleSimUtils
command: |
brew update
brew tap wix/brew
brew install wix/brew/applesimutils
- run:
name: Install NPM modules
command: |
yarn global add detox-cli
yarn
- run:
name: Rebuild Detox framework cache
command: |
detox clean-framework-cache
detox build-framework-cache
- run:
name: Test
command: |
......@@ -96,9 +158,6 @@ jobs:
paths:
- node_modules
- store_artifacts:
path: /tmp/screenshots
android-build:
<<: *defaults
docker:
......@@ -359,9 +418,12 @@ workflows:
type: approval
requires:
- lint-testunit
- e2e-test:
- e2e-build:
requires:
- e2e-hold
- e2e-test:
requires:
- e2e-build
- ios-build:
requires:
......
import React from 'react';
import { FlatList } from 'react-native';
import { FlatList, View } from 'react-native';
import PropTypes from 'prop-types';
import equal from 'deep-equal';
......@@ -12,15 +12,16 @@ const Mentions = React.memo(({ mentions, trackingType, theme }) => {
return null;
}
return (
<FlatList
testID='messagebox-container'
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
data={mentions}
extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
keyExtractor={item => item.id || item.username || item.command || item}
keyboardShouldPersistTaps='always'
/>
<View testID='messagebox-container'>
<FlatList
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
data={mentions}
extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
keyExtractor={item => item.id || item.username || item.command || item}
keyboardShouldPersistTaps='always'
/>
</View>
);
}, (prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
......
......@@ -151,6 +151,7 @@ class Markdown extends PureComponent {
];
return (
<Text
accessibilityLabel={literal}
style={[styles.text, defaultStyle, ...style]}
numberOfLines={numberOfLines}
>
......
......@@ -21,6 +21,7 @@ const Broadcast = React.memo(({
background={Touchable.Ripple(themes[theme].bannerBackground)}
style={[styles.button, { backgroundColor: themes[theme].tintColor }]}
hitSlop={BUTTON_HIT_SLOP}
testID='message-broadcast-reply'
>
<>
<CustomIcon name='back' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
......
......@@ -11,7 +11,14 @@ import { themes } from '../../constants/colors';
const Content = React.memo((props) => {
if (props.isInfo) {
return <Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{getInfoMessage({ ...props })}</Text>;
const infoMessage = getInfoMessage({ ...props });
return (
<Text
style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}
accessibilityLabel={infoMessage}
>{infoMessage}
</Text>
);
}
let content = null;
......
......@@ -166,10 +166,12 @@ const handleLogout = function* handleLogout({ forcedByServer }) {
// see if there're other logged in servers and selects first one
if (servers.length > 0) {
const newServer = servers[0].id;
const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`);
if (token) {
return yield put(selectServerRequest(newServer));
for (let i = 0; i < servers.length; i += 1) {
const newServer = servers[i].id;
const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`);
if (token) {
return yield put(selectServerRequest(newServer));
}
}
}
// if there's no servers, go outside
......
......@@ -242,7 +242,6 @@ class NotificationPreferencesView extends React.Component {
{...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
testID='notification-preference-view-list'
>
<Separator theme={theme} />
......
......@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { View, ScrollView, Keyboard } from 'react-native';
import { connect } from 'react-redux';
import Dialog from 'react-native-dialog';
import prompt from 'react-native-prompt-android';
import SHA256 from 'js-sha256';
import ImagePicker from 'react-native-image-crop-picker';
import RNPickerSelect from 'react-native-picker-select';
......@@ -61,7 +61,6 @@ class ProfileView extends React.Component {
}
state = {
showPasswordAlert: false,
saving: false,
name: null,
username: null,
......@@ -155,19 +154,11 @@ class ProfileView extends React.Component {
);
}
closePasswordAlert = () => {
this.setState({ showPasswordAlert: false });
}
handleError = (e, func, action) => {
if (e.data && e.data.errorType === 'error-too-many-requests') {
return showErrorAlert(e.data.error);
}
showErrorAlert(
I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }),
'',
() => this.setState({ showPasswordAlert: false })
);
showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
}
submit = async() => {
......@@ -212,7 +203,26 @@ class ProfileView extends React.Component {
const requirePassword = !!params.email || newPassword;
if (requirePassword && !params.currentPassword) {
return this.setState({ showPasswordAlert: true, saving: false });
this.setState({ saving: false });
prompt(
I18n.t('Please_enter_your_password'),
I18n.t('For_your_security_you_must_enter_your_current_password_to_continue'),
[
{ text: I18n.t('Cancel'), onPress: () => {}, style: 'cancel' },
{
text: I18n.t('Save'),
onPress: (p) => {
this.setState({ currentPassword: p });
this.submit();
}
}
],
{
type: 'secure-text',
cancelable: false
}
);
return;
}
try {
......@@ -233,7 +243,7 @@ class ProfileView extends React.Component {
} else {
setUser({ ...params });
}
this.setState({ saving: false, showPasswordAlert: false });
this.setState({ saving: false });
EventEmitter.emit(LISTENER, { message: I18n.t('Profile_saved_successfully') });
this.init();
}
......@@ -409,7 +419,7 @@ class ProfileView extends React.Component {
render() {
const {
name, username, email, newPassword, avatarUrl, customFields, avatar, saving, showPasswordAlert
name, username, email, newPassword, avatarUrl, customFields, avatar, saving
} = this.state;
const {
baseUrl,
......@@ -533,22 +543,6 @@ class ProfileView extends React.Component {
loading={saving}
theme={theme}
/>
<Dialog.Container visible={showPasswordAlert}>
<Dialog.Title>
{I18n.t('Please_enter_your_password')}
</Dialog.Title>
<Dialog.Description>
{I18n.t('For_your_security_you_must_enter_your_current_password_to_continue')}
</Dialog.Description>
<Dialog.Input
onChangeText={value => this.setState({ currentPassword: value })}
secureTextEntry
testID='profile-view-typed-password'
style={styles.dialogInput}
/>
<Dialog.Button label={I18n.t('Cancel')} onPress={this.closePasswordAlert} />
<Dialog.Button label={I18n.t('Save')} onPress={this.submit} />
</Dialog.Container>
</ScrollView>
</SafeAreaView>
</KeyboardView>
......
import { StyleSheet, Platform } from 'react-native';
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
disabled: {
......@@ -23,14 +23,5 @@ export default StyleSheet.create({
marginRight: 15,
marginBottom: 15,
borderRadius: 2
},
dialogInput: Platform.select({
ios: {},
android: {
borderRadius: 4,
borderColor: 'rgba(0,0,0,.15)',
borderWidth: 2,
paddingHorizontal: 10
}
})
}
});
......@@ -452,7 +452,7 @@ class RoomInfoEditView extends React.Component {
]}
onPress={this.toggleArchive}
disabled={!this.hasArchivePermission()}
testID='room-info-edit-view-archive'
testID={archived ? 'room-info-edit-view-unarchive' : 'room-info-edit-view-archive'}
>
<Text
style={[
......@@ -460,7 +460,6 @@ class RoomInfoEditView extends React.Component {
styles.button_inverted,
{ color: dangerColor }
]}
accessibilityTraits='button'
>
{ archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') }
</Text>
......
......@@ -173,7 +173,7 @@ class RoomInfoView extends React.Component {
const { theme } = this.props;
return (
<View style={styles.item}>
<Text style={[styles.itemLabel, { color: themes[theme].auxiliaryText }]}>{I18n.t(camelize(key))}</Text>
<Text accessibilityLabel={key} style={[styles.itemLabel, { color: themes[theme].auxiliaryText }]}>{I18n.t(camelize(key))}</Text>
<Markdown
msg={room[key] ? room[key] : `__${ I18n.t(`No_${ key }_provided`) }__`}
theme={theme}
......
......@@ -116,7 +116,11 @@ const Header = React.memo(({
};
return (
<TouchableOpacity onPress={onPress} style={[styles.container, { width: width - widthOffset }]}>
<TouchableOpacity
testID='room-view-header-actions'
onPress={onPress}
style={[styles.container, { width: width - widthOffset }]}
>
<View style={[styles.titleContainer, tmid && styles.threadContainer]}>
<ScrollView
showsHorizontalScrollIndicator={false}
......
......@@ -854,7 +854,7 @@ class RoomView extends React.Component {
if (!joined && !this.tmid) {
return (
<View style={styles.joinRoomContainer} key='room-view-join' testID='room-view-join'>
<Text style={[styles.previewMode, { color: themes[theme].titleText }]}>{I18n.t('You_are_in_preview_mode')}</Text>
<Text accessibilityLabel={I18n.t('You_are_in_preview_mode')} style={[styles.previewMode, { color: themes[theme].titleText }]}>{I18n.t('You_are_in_preview_mode')}</Text>
<Touch
onPress={this.joinRoom}
style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]}
......@@ -868,7 +868,7 @@ class RoomView extends React.Component {
if (this.isReadOnly || room.archived) {
return (
<View style={styles.readOnly}>
<Text style={[styles.previewMode, { color: themes[theme].titleText }]}>{I18n.t('This_room_is_read_only')}</Text>
<Text style={[styles.previewMode, { color: themes[theme].titleText }]} accessibilityLabel={I18n.t('This_room_is_read_only')}>{I18n.t('This_room_is_read_only')}</Text>
</View>
);
}
......
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
describe('Onboarding', () => {
......@@ -25,10 +24,6 @@ describe('Onboarding', () => {
it('should have "Create a new workspace"', async() => {
await expect(element(by.id('create-workspace-button'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
......@@ -40,12 +35,12 @@ describe('Onboarding', () => {
await element(by.id('join-community-button')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('welcome-view'))).toBeVisible();
await waitFor(element(by.text('Rocket.Chat'))).toBeVisible().withTimeout(60000);
await expect(element(by.text('Rocket.Chat'))).toBeVisible();
// await waitFor(element(by.text('Rocket.Chat'))).toBeVisible().withTimeout(60000);
// await expect(element(by.text('Rocket.Chat'))).toBeVisible();
});
it('should navigate to new server', async() => {
await device.reloadReactNative();
await device.launchApp({ newInstance: true });
await waitFor(element(by.id('onboarding-view'))).toBeVisible().withTimeout(2000);
await element(by.id('connect-server-button')).tap();
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(60000);
......@@ -55,7 +50,7 @@ describe('Onboarding', () => {
it('should enter an invalid server and get error', async() => {
await element(by.id('new-server-view-input')).replaceText('invalidtest');
await element(by.id('new-server-view-button')).tap();
const errorText = 'The URL you entered is invalid. Check it and try again, please!';
const errorText = 'Oops!';
await waitFor(element(by.text(errorText))).toBeVisible().withTimeout(60000);
await expect(element(by.text(errorText))).toBeVisible();
});
......@@ -69,7 +64,7 @@ describe('Onboarding', () => {
});
it('should enter a valid server without login services and navigate to login', async() => {
await device.reloadReactNative();
await device.launchApp({ newInstance: true });
await waitFor(element(by.id('onboarding-view'))).toBeVisible().withTimeout(2000);
await element(by.id('connect-server-button')).tap();
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(60000);
......@@ -78,10 +73,5 @@ describe('Onboarding', () => {
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('login-view'))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
});
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const { tapBack } = require('./helpers/app');
describe('Welcome screen', () => {
before(async() => {
await device.reloadReactNative();
await device.launchApp({ newInstance: true });
await element(by.id('join-community-button')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000);
})
......@@ -25,10 +24,6 @@ describe('Welcome screen', () => {
});
// TODO: oauth
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
......@@ -51,9 +46,5 @@ describe('Welcome screen', () => {
await waitFor(element(by.id('legal-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('legal-view'))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
});
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const { tapBack } = require('./helpers/app');
describe('Legal screen', () => {
......@@ -22,10 +21,6 @@ describe('Legal screen', () => {
it('should have privacy policy button', async() => {
await expect(element(by.id('legal-privacy-button'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
......@@ -48,9 +43,5 @@ describe('Legal screen', () => {
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('welcome-view'))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
});
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const data = require('./data');
describe('Forgot password screen', () => {
......@@ -24,10 +23,6 @@ describe('Forgot password screen', () => {
it('should have submit button', async() => {
await expect(element(by.id('forgot-password-view-submit'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', async() => {
......@@ -38,9 +33,5 @@ describe('Forgot password screen', () => {
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('login-view'))).toBeVisible();
});
afterEach(async() => {
takeScreenshot();
});
});
});
const {
device, expect, element, by, waitFor
} = require('detox');
const { takeScreenshot } = require('./helpers/screenshot');
const { logout, sleep } = require('./helpers/app');
const data = require('./data');
......@@ -19,7 +18,7 @@ async function navigateToRegister() {
describe('Create user screen', () => {
before(async() => {
await device.reloadReactNative();
await device.launchApp({ newInstance: true });
await navigateToRegister();
});
......@@ -51,15 +50,11 @@ describe('Create user screen', () => {
it('should have legal button', async() => {
await expect(element(by.id('register-view-more'))).toBeVisible();
});
after(async() => {
takeScreenshot();
});
});
describe('Usage', () => {
// FIXME: Detox isn't able to check if it's tappable: https://github.com/wix/Detox/issues/246
// it.only('should submit invalid email and do nothing', async() => {
// it('should submit invalid email and do nothing', async() => {
// const invalidEmail = 'invalidemail';
// await element(by.id('register-view-name')).replaceText(data.user);
// await element(by.id('register-view-username')).replaceText(data.user);
......@@ -74,18 +69,20 @@ describe('Create user screen', () => {
await element(by.id('register-view-username')).replaceText(data.user);
await element(by.id('register-view-email')).replaceText(data.existingEmail);
await element(by.id('register-view-password')).replaceText(data.password);
await sleep(300);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Email already exists. [403]')).atIndex(0)).toExist().withTimeout(10000);
await expect(element(by.text('Email already exists. [403]')).atIndex(0)).toExist();
await element(by.text('OK')).tap();
});
it('should submit email already taken and raise error', async() => {
it('should submit username already taken and raise error', async() => {
const invalidEmail = 'invalidemail';
await element(by.id('register-view-name')).replaceText(data.user);
await element(by.id('register-view-username')).replaceText(data.existingName);
await element(by.id('register-view-email')).replaceText(data.email);
await element(by.id('register-view-password')).replaceText(data.password);
await sleep(300);
await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Username is already in use')).atIndex(0)).toExist().withTimeout(10000);
await expect(element(by.text('Username is already in use')).atIndex(0)).toExist();
......@@ -97,15 +94,12 @@ describe('Create user screen', () => {