From a9a4845778cc41c0525a01d9b442cb6f00a4aca7 Mon Sep 17 00:00:00 2001 From: Wesley Chang Date: Tue, 19 Nov 2019 17:24:52 -0800 Subject: [PATCH 1/3] Add detox tests --- frontend/android/app/build.gradle | 4 + .../java/com/jobtogo/DetoxTest.java | 24 +++ frontend/android/build.gradle | 8 +- frontend/e2e/config.json | 6 + frontend/e2e/firstTest.spec.js | 194 ++++++++++++++++++ frontend/e2e/init.js | 25 +++ frontend/package.json | 13 ++ frontend/src/App.jsx | 4 + frontend/src/components/Button/Button.jsx | 5 +- .../src/components/IconButton/IconButton.jsx | 5 +- .../components/ImageButton/ImageButton.jsx | 7 +- .../components/InfoDisplay/InfoDisplay.jsx | 4 +- frontend/src/components/JobCard/JobCard.jsx | 6 +- .../src/components/JobDetails/JobDetails.jsx | 10 +- .../JobShareModal/JobShareModal.jsx | 6 +- .../src/components/MainHeader/MainHeader.jsx | 3 +- .../src/components/NavHeader/NavHeader.jsx | 10 +- .../src/components/SearchBar/SearchBar.jsx | 2 + .../SelectableItem/SelectableItem.jsx | 6 +- .../SwitchableNav/SwitchableNav.jsx | 4 +- .../src/screens/AuthLanding/AuthLanding.jsx | 5 + .../src/screens/EditFriends/EditFriends.jsx | 4 + .../src/screens/EditSkills/EditSkills.jsx | 7 +- frontend/src/screens/JobSwipe/JobSwipe.jsx | 3 +- .../screens/SendLikedJobs/SendLikedJobs.jsx | 6 +- frontend/src/screens/SignIn/SignIn.jsx | 12 +- 26 files changed, 360 insertions(+), 23 deletions(-) create mode 100644 frontend/android/app/src/androidTest/java/com/jobtogo/DetoxTest.java create mode 100644 frontend/e2e/config.json create mode 100644 frontend/e2e/firstTest.spec.js create mode 100644 frontend/e2e/init.js diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle index 3dba9f53..c8754aff 100644 --- a/frontend/android/app/build.gradle +++ b/frontend/android/app/build.gradle @@ -138,6 +138,8 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" + testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } splits { abi { @@ -213,6 +215,8 @@ dependencies { } else { implementation jscFlavor } + androidTestImplementation('com.wix:detox:+') { transitive = true } + androidTestImplementation 'junit:junit:4.12' } // Run this once to be able to run the application with BUCK diff --git a/frontend/android/app/src/androidTest/java/com/jobtogo/DetoxTest.java b/frontend/android/app/src/androidTest/java/com/jobtogo/DetoxTest.java new file mode 100644 index 00000000..8518f9b2 --- /dev/null +++ b/frontend/android/app/src/androidTest/java/com/jobtogo/DetoxTest.java @@ -0,0 +1,24 @@ +package com.jobtogo; + +import com.wix.detox.Detox; + +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 + public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); + + @Test + public void runDetoxTests() { + Detox.runTests(mActivityRule); + } +} \ No newline at end of file diff --git a/frontend/android/build.gradle b/frontend/android/build.gradle index 2b4a2ea8..342418c9 100644 --- a/frontend/android/build.gradle +++ b/frontend/android/build.gradle @@ -2,8 +2,9 @@ buildscript { ext { + kotlinVersion = '1.3.10' buildToolsVersion = "28.0.3" - minSdkVersion = 16 + minSdkVersion = 18 compileSdkVersion = 28 targetSdkVersion = 28 supportLibVersion = "28.0.0" @@ -17,6 +18,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:3.4.1") classpath 'com.google.gms:google-services:4.3.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -36,6 +38,10 @@ allprojects { url("$rootDir/../node_modules/jsc-android/dist") } + maven { + // All of Detox' artifacts are provided via the npm module + url "$rootDir/../node_modules/detox/Detox-android" + } jcenter() } } diff --git a/frontend/e2e/config.json b/frontend/e2e/config.json new file mode 100644 index 00000000..9ca293f6 --- /dev/null +++ b/frontend/e2e/config.json @@ -0,0 +1,6 @@ +{ + "setupFilesAfterEnv": ["./init.js"], + "testEnvironment": "node", + "reporters": ["detox/runners/jest/streamlineReporter"], + "verbose": true +} \ No newline at end of file diff --git a/frontend/e2e/firstTest.spec.js b/frontend/e2e/firstTest.spec.js new file mode 100644 index 00000000..7ee3c23d --- /dev/null +++ b/frontend/e2e/firstTest.spec.js @@ -0,0 +1,194 @@ +beforeEach(async () => { + await device.reloadReactNative(); + await element(by.id('signInAuth')).tap(); + element(by.id('email')).typeText('a\n'); + await waitFor(element(by.id('password'))).toBeVisible().withTimeout(2000); + element(by.id('password')).typeText('a\n'); + await waitFor(element(by.id('signIn'))).toBeVisible().withTimeout(2000); + await element(by.id('signIn')).tap(); +}); + +describe('Auth Screens', () => { + it('Check AuthLanding component', async () => { + await device.reloadReactNative(); + await expect(element(by.id('logoLight'))).toBeVisible(); + await expect(element(by.id('jobSeeker'))).toBeVisible(); + await expect(element(by.id('google'))).toBeVisible(); + await expect(element(by.id('signInAuth'))).toBeVisible(); + + await element(by.id('signInAuth')).tap(); + await expect(element(by.id('logoSignin'))).toBeVisible(); + await expect(element(by.id('email'))).toBeVisible(); + await expect(element(by.id('password'))).toBeVisible(); + await expect(element(by.id('signIn'))).toBeVisible(); + }); + + it('Correct login sequence', async () => { + await device.reloadReactNative(); + await element(by.id('signInAuth')).tap(); + await element(by.id('email')).typeText('a\n'); + await element(by.id('signInAuth')).tap(); + await waitFor(element(by.id('password'))).toBeVisible().withTimeout(2000); + await element(by.id('password')).typeText('a\n'); + await waitFor(element(by.id('signIn'))).toBeVisible().withTimeout(2000); + await element(by.id('signIn')).tap(); + await waitFor(element(by.id('jobSwipe'))).toBeVisible(); + }); + + it('Incorrect login sequence', async () => { + await device.reloadReactNative(); + await element(by.id('signInAuth')).tap(); + await element(by.id('email')).typeText('\n'); + await waitFor(element(by.id('password'))).toBeVisible().withTimeout(2000); + await element(by.id('password')).typeText('\n'); + await waitFor(element(by.id('signIn'))).toBeVisible().withTimeout(2000); + // await element(by.id('signIn')).tap(); + await expect(element(by.id('loginErrorMsg'))).toBeVisible(); + // TODO: test for difference error responses + }); +}); + +describe('Home Screen', () => { + it('Check Jobswipe components', async () => { + await expect(element(by.id('mainHeader'))).toBeVisible(); + await expect(element(by.id('inbox'))).toBeVisible(); + // Check the inbox model + await element(by.id('inbox')).tap(); + await expect(element(by.id('mainHeader'))).toBeVisible(); + await expect(element(by.id('infoDisplay'))).toBeVisible(); + + // Switch back from the inbox model + await element(by.id('inbox')).tap(); + await expect(element(by.id('mainHeader'))).toBeVisible(); + }); + + it('Check share modal', async () => { + // Check the share job modal + await element(by.id('card0')).tap(); + await expect(element(by.id('shareModal'))).toBeVisible(); + + // Close the share job modal + await element(by.id('closeShare')).tap(); + await expect(element(by.id('shareModal'))).toBeNotVisible(); + await expect(element(by.id('mainHeader'))).toBeVisible(); + }); + + it('Check info modal', async () => { + // Check the share job modal + await element(by.id('card0Open')).tap(); + await waitFor(element(by.id('card0Description'))).toBeVisible().withTimeout(2000); + await expect(element(by.id('card0Description'))).toBeVisible(); + + // Close the share job modal + await element(by.id('card0Close')).tap(); + await expect(element(by.id('card0Description'))).toBeNotVisible(); + }); +}); + +describe('Liked Screen', () => { + it('Check Liked components', async () => { + await element(by.id('liked')).tap(); + await expect(element(by.id('navHeaderLiked'))).toBeVisible(); + await expect(element(by.id('search'))).toBeVisible(); + await expect(element(by.id('user'))).toBeVisible(); + await expect(element(by.text('Liked Jobs'))).toBeVisible(); + + await expect(element(by.id('sendJobs'))).toBeVisible(); + await expect(element(by.id('likedJobs'))).toBeVisible(); + await expect(element(by.id('jobItem0'))).toBeVisible(); + }); +}); + +describe('Friends Screen', () => { + it('Check Friends components', async () => { + await element(by.id('friends')).tap(); + + // Nav header + await expect(element(by.id('navHeaderFriends'))).toBeVisible(); + await expect(element(by.id('search'))).toBeVisible(); + await expect(element(by.id('user'))).toBeVisible(); + + // Switch navigator + await expect(element(by.id('switchNav'))).toBeVisible(); + await expect(element(by.id('switchNavOption1'))).toBeVisible(); + await expect(element(by.id('switchNavOption2'))).toBeVisible(); + await expect(element(by.text('Pending Friends'))).toBeVisible(); + await expect(element(by.text('Your Friends'))).toBeVisible(); + + // User should have no pending friends + await expect(element(by.id('infoDisplay'))).toBeVisible(); + }); + + it('Navigate between Pending/Your Friends', async () => { + await element(by.id('friends')).tap(); + await element(by.id('switchNavOption2')).tap(); + await element(by.id('switchNavOption1')).tap(); + + // xfail once friends are added to user + await expect(element(by.id('infoDisplay'))).toBeVisible(); + }); + + it('Display 100 friends', async () => { + const NON_FUNC_REQ_FRIEND_COUNT = 100; + await element(by.id('friends')).tap(); + await element(by.id('switchNavOption2')).tap(); + + for (i = 0; i < NON_FUNC_REQ_FRIEND_COUNT; i++) { + await waitFor(element(by.id(`userItem${i}`))) + .toBeVisible().whileElement(by.id('userList')).scroll(300, 'down'); + } + }); + + it('Check user search component', async () => { + await element(by.id('friends')).tap(); + await element(by.id('search')).tap(); + await expect(element(by.id('searchBack'))).toBeVisible(); + await expect(element(by.id('searchInput'))).toBeVisible(); + }); + + it('Users search sequence', async () => { + await element(by.id('friends')).tap(); + await element(by.id('search')).tap(); + + await expect(element(by.id('userList'))).toBeNotVisible(); + + element(by.id('searchInput')).typeText('search query\n'); + await element(by.id('searchBack')).tap(); + + await expect(element(by.id('userList'))).toBeVisible(); + + // Go back to previous screen + await expect(element(by.id('editFriends'))).toBeVisible(); + }); +}); + +describe('Resume Page', () => { + it('Check resume components', async () => { + await element(by.id('resume')).tap(); + await expect(element(by.id('navHeaderSkills'))).toBeVisible(); + await expect(element(by.id('textSkills'))).toBeVisible(); + await expect(element(by.id('checkingDoc'))).toBeVisible(); + await expect(element(by.id('uploadResume'))).toBeVisible(); + // await element(by.id('uploadResume')).tap(); + // todo + }); +}); + +describe('Navigation', () => { + it('Navigation between pages', async () => { + // Check navigation buttons + await expect(element(by.id('home'))).toBeVisible(); + await expect(element(by.id('liked'))).toBeVisible(); + await expect(element(by.id('friends'))).toBeVisible(); + await expect(element(by.id('resume'))).toBeVisible(); + + await element(by.id('friends')).tap(); + await expect(element(by.id('editFriends'))).toBeVisible(); + await element(by.id('resume')).tap(); + await expect(element(by.id('editSkills'))).toBeVisible(); + await element(by.id('home')).tap(); + await expect(element(by.id('jobSwipe'))).toBeVisible(); + await element(by.id('liked')).tap(); + await expect(element(by.id('sendLikedJobs'))).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/frontend/e2e/init.js b/frontend/e2e/init.js new file mode 100644 index 00000000..969049ec --- /dev/null +++ b/frontend/e2e/init.js @@ -0,0 +1,25 @@ +const detox = require('detox'); +const config = require('../package.json').detox; +const adapter = require('detox/runners/jest/adapter'); +const specReporter = require('detox/runners/jest/specReporter'); + +// Set the default timeout +jest.setTimeout(120000); +jasmine.getEnv().addReporter(adapter); + +// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level. +// This is strictly optional. +jasmine.getEnv().addReporter(specReporter); + +beforeAll(async () => { + await detox.init(config); +}); + +beforeEach(async () => { + await adapter.beforeEach(); +}); + +afterAll(async () => { + await adapter.afterAll(); + await detox.cleanup(); +}); \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 60523126..ad955f9b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,5 +54,18 @@ }, "jest": { "preset": "react-native" + }, + "detox": { + "test-runner": "jest", + "configurations": { + "android.emu.debug": { + "binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk", + "build": "cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..", + "type": "android.emulator", + "device": { + "avdName": "Nexus_5X_API_28" + } + } + } } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 86faabec..677c483e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -130,6 +130,7 @@ const AppStack = createBottomTabNavigator( screen: JobSwipe, navigationOptions: { tabBarLabel: 'Home', + tabBarTestID: "home", tabBarIcon: HomeTabIcon, }, }, @@ -137,6 +138,7 @@ const AppStack = createBottomTabNavigator( screen: SendLikedJobs, navigationOptions: { tabBarLabel: 'Liked', + tabBarTestID: "liked", tabBarIcon: LikedTabIcon, }, }, @@ -144,6 +146,7 @@ const AppStack = createBottomTabNavigator( screen: EditFriends, navigationOptions: { tabBarLabel: 'Friends', + tabBarTestID: "friends", tabBarIcon: FriendsTabIcon, }, }, @@ -151,6 +154,7 @@ const AppStack = createBottomTabNavigator( screen: EditSkills, navigationOptions: { tabBarLabel: 'Resume', + tabBarTestID: "resume", tabBarIcon: ResumeTabIcon, }, }, diff --git a/frontend/src/components/Button/Button.jsx b/frontend/src/components/Button/Button.jsx index 29de3a47..bbb77cbe 100644 --- a/frontend/src/components/Button/Button.jsx +++ b/frontend/src/components/Button/Button.jsx @@ -5,9 +5,10 @@ import PropTypes from 'prop-types'; import styles from './styles'; const Button = ({ - style, backgroundColor, textColor, title, enable, onPress, + style, backgroundColor, textColor, title, enable, onPress, testID, }) => ( ( @@ -20,6 +21,7 @@ const IconButton = ({ IconButton.defaultProps = { color: colours.gray, size: sizes.icon, + testID: '', }; IconButton.propTypes = { @@ -27,6 +29,7 @@ IconButton.propTypes = { color: string, size: number, onPress: func.isRequired, + testID: string, }; export default IconButton; diff --git a/frontend/src/components/ImageButton/ImageButton.jsx b/frontend/src/components/ImageButton/ImageButton.jsx index 8fc5dfb4..d236f837 100644 --- a/frontend/src/components/ImageButton/ImageButton.jsx +++ b/frontend/src/components/ImageButton/ImageButton.jsx @@ -1,11 +1,12 @@ import React from 'react'; import { TouchableOpacity, Image } from 'react-native'; -import { number, func } from 'prop-types'; +import { number, func, string } from 'prop-types'; import styles from './styles'; -const ImageButton = ({ source, onPress }) => ( +const ImageButton = ({ source, onPress, testID }) => ( @@ -17,11 +18,13 @@ const ImageButton = ({ source, onPress }) => ( ImageButton.defaultProps = { onPress: () => {}, + testID: '', }; ImageButton.propTypes = { source: number.isRequired, onPress: func, + testID: string, }; export default ImageButton; diff --git a/frontend/src/components/InfoDisplay/InfoDisplay.jsx b/frontend/src/components/InfoDisplay/InfoDisplay.jsx index f899293f..60989855 100644 --- a/frontend/src/components/InfoDisplay/InfoDisplay.jsx +++ b/frontend/src/components/InfoDisplay/InfoDisplay.jsx @@ -7,8 +7,8 @@ import { errors } from '../../constants/messages'; import styles from './styles'; -const InfoDisplay = ({ message, source }) => ( - +const InfoDisplay = ({ message, source, }) => ( + ( @@ -35,8 +35,12 @@ class JobDetails extends Component { transparent visible={modalVisible} > - + - + diff --git a/frontend/src/components/MainHeader/MainHeader.jsx b/frontend/src/components/MainHeader/MainHeader.jsx index 033deb35..69811266 100644 --- a/frontend/src/components/MainHeader/MainHeader.jsx +++ b/frontend/src/components/MainHeader/MainHeader.jsx @@ -8,9 +8,10 @@ import styles from './styles'; import { colours, sizes } from '../../styles'; const MainHeader = ({ onPress }) => ( - + { let button; if (buttonOption === 'search') { button = ( + {button} - {title} + {title} ); @@ -58,12 +60,14 @@ const NavHeader = ({ NavHeader.defaultProps = { buttonOption: 'none', onPressButton: () => {}, + testID: '', }; NavHeader.propTypes = { title: string.isRequired, buttonOption: oneOf(['search', 'back', 'none']), onPressButton: func, + testID: string, }; export default NavHeader; diff --git a/frontend/src/components/SearchBar/SearchBar.jsx b/frontend/src/components/SearchBar/SearchBar.jsx index ad2fae25..cea0d790 100644 --- a/frontend/src/components/SearchBar/SearchBar.jsx +++ b/frontend/src/components/SearchBar/SearchBar.jsx @@ -9,12 +9,14 @@ import { colours, sizes } from '../../styles'; const SearchBar = ({ value, onChangeText, onEndSearch }) => ( ( - + ( - + @@ -21,6 +22,7 @@ const SwitchableNav = ({ diff --git a/frontend/src/screens/AuthLanding/AuthLanding.jsx b/frontend/src/screens/AuthLanding/AuthLanding.jsx index 8239c0e0..397df402 100644 --- a/frontend/src/screens/AuthLanding/AuthLanding.jsx +++ b/frontend/src/screens/AuthLanding/AuthLanding.jsx @@ -55,15 +55,18 @@ export default class AuthLanding extends Component { return (