Skip to content
Commits on Source (11)
......@@ -68,9 +68,19 @@ jobs:
- restore_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}
# remove detox from CI until is fixed
# - run:
# name: Install detox
# command:
# |
# HOMEBREW_NO_AUTO_UPDATE=1 brew tap wix/brew
# HOMEBREW_NO_AUTO_UPDATE=1 brew install --HEAD applesimutils
# npm install -g detox-cli
# npm install -g detox
# not using a workspace here as Node and Yarn versions
# differ between our macOS executor image and the Docker containers above
- run: yarn install
- run: yarn install --frozen-lockfile
- save_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
......@@ -107,6 +117,11 @@ jobs:
paths:
- ios/Pods
# remove detox from CI until is fixed
# Run e2e
# - run: detox build -c ios.sim.release
# - run: detox test -c ios.sim.release --cleanup
### TODO- get tests running with fastlane
......
......@@ -36,29 +36,6 @@ build:android:
expire_in: 7 days
when: on_success
e2e:browserstacks:
image: node:10.16.3
stage: e2e
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- yarn install
- export bsAPP=`curl -u "${bsUSER}:${bsKEY}" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@./Minds-$CI_BUILD_REF_SLUG.apk"| grep -o 'bs\:\/\/.*"' | sed 's/.$//'`
- yarn e2e
tags:
- docker
dependencies:
- build:android
only:
refs:
- /^stable-*/
- /^release-*/
- /^feat-*/
- /^test-*/
allow_failure: true
deploy:s3:
image: minds/ci:latest
stage: deploy
......
......@@ -37,5 +37,18 @@
- `yarn test`
## Testing e2e (macOS)
Install the detox cli
- `brew tap wix/brew`
- `brew install applesimutils`
- `yarn global add detox-cli`
Run the tests
- `detox build -c ios.sim.debug`
- `detox test -c ios.sim.debug`
You can use -c ios.sim.release for e2e test a production build
### _Copyright Minds 2018_
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver) => {
// should ask for permissions
const permmision = await driver.waitForElementById('com.android.packageinstaller:id/permission_allow_button', wd.asserters.isDisplayed, 10000)
// we accept
permmision.click();
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver) => {
// select first image
const firstImage = await driver.waitForElementByAccessibilityId('Gallery image/jpeg', wd.asserters.isDisplayed, 5000);
await firstImage.click();
await sleep(3000);
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver, amount) => {
const lockButton = await driver.waitForElementByAccessibilityId('Post lock button', wd.asserters.isDisplayed, 5000);
await lockButton.click();
const postInput = await driver.waitForElementByAccessibilityId('Poster lock amount input', wd.asserters.isDisplayed, 5000);
await postInput.type(amount);
// we press post button
const postButton = await driver.elementByAccessibilityId('Poster lock done button');
await postButton.click();
}
\ No newline at end of file
export default async(driver) => {
const username = await driver.elementByAccessibilityId('username input');
const password = await driver.elementByAccessibilityId('password input');
const loginButton = await driver.elementByAccessibilityId('login button');
await username.type(process.env.loginUser);
await password.type(process.env.loginPass);
await loginButton.click();
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver, text) => {
// post screen must be shown
const postInput = await driver.waitForElementByAccessibilityId('PostInput', wd.asserters.isDisplayed, 5000);
await postInput.type(text);
// we press post button
const postButton = await driver.elementByAccessibilityId('Capture Post Button');
await postButton.click();
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver) => {
// tap the capture button
const button = await driver.waitForElementByAccessibilityId('CaptureButton', wd.asserters.isDisplayed, 10000);
button.click();
return button;
}
\ No newline at end of file
import wd from 'wd';
import sleep from '../../src/common/helpers/sleep';
export default async(driver, options) => {
// tap the toggle button
const button = await driver.waitForElementByAccessibilityId('NSFW button', wd.asserters.isDisplayed, 5000);
await button.click();
// wait until the menu is shown
await sleep(500);
for (let index = 0; index < options.length; index++) {
const name = options[index];
const element = await driver.elementByAccessibilityId(`NSFW ${name}`);
await element.click();
}
let action = new wd.TouchAction(driver);
action.tap({x:100, y:170});
await action.release().perform();
}
\ No newline at end of file
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import post from './actions/post';
import login from './actions/login';
import { driver, capabilities} from './config';
import sleep from '../src/common/helpers/sleep';
import pressCapture from './actions/pressCapture';
import acceptPermissions from './actions/acceptPermissions';
import attachPostGalleryImage from './actions/attachPostGalleryImage';
import selectNsfw from './actions/selectNsfw';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
//TODO: add support for ios to this test (xpath)
describe('Activity flow tests', () => {
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
// we should login
await login(driver);
});
afterAll(async () => {
await driver.quit();
});
it('should post a text and see it in the newsfeed', async () => {
const str = 'My e2e activity';
// press capture button
await pressCapture(driver);
// accept gallery permissions
await acceptPermissions(driver);
// make the post
await post(driver, str);
// should post and return to the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 10000);
// the first element of the list should be the post
const textElement = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Newsfeed Screen"]/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[2]/android.view.ViewGroup/android.widget.TextView[2]');
expect(await textElement.text()).toBe(str);
});
it('should like the post', async() => {
const likeButton = await driver.waitForElementByAccessibilityId('Thumb up activity button', 5000);
await likeButton.click();
const likeCount = await driver.waitForElementByAccessibilityId('Thumb up count', 5000);
expect(await likeCount.text()).toBe('1');
});
it('should unlike the post', async() => {
const likeButton = await driver.waitForElementByAccessibilityId('Thumb down activity button', 5000);
await likeButton.click();
const likeCount = await driver.waitForElementByAccessibilityId('Thumb down count', 5000);
expect(await likeCount.text()).toBe('1');
});
});
\ No newline at end of file
import factory from '../tests-helpers/e2e-driver.factory';
const customCapabilities = {
'device' : 'Samsung Galaxy S9',
'os_version' : '8.0'
};
let driver, capabilities;
if (process.env.e2elocal) {
[driver, capabilities] = factory('androidLocal', {});
} else {
[driver, capabilities] = factory('browserStack', customCapabilities);
}
export {driver, capabilities} ;
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import { driver, capabilities} from './config';
import post from './actions/post';
import login from './actions/login';
import sleep from '../src/common/helpers/sleep';
import pressCapture from './actions/pressCapture';
import acceptPermissions from './actions/acceptPermissions';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
describe('Discovery post edit flow', () => {
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
// we should login
await login(driver);
});
afterAll(async () => {
await driver.quit();
});
it('should post a text and go to discovery', async () => {
const str = 'My e2e post #mye2epost';
await pressCapture(driver);
await acceptPermissions(driver);
// make the post
await post(driver, str);
// move to discovery
const discoveryTab = await driver.waitForElementByAccessibilityId('Discovery tab button', wd.asserters.isDisplayed, 10000);
await discoveryTab.click();
});
it('should search for the post', async () => {
// select all list
const all = await driver.waitForElementByAccessibilityId('Discovery All', wd.asserters.isDisplayed, 5000);
await all.click();
await sleep(500);
// select latest filter
const latest = await driver.waitForElementByAccessibilityId('Filter latest button', wd.asserters.isDisplayed, 1000);
await latest.click();
await sleep(500);
// search the post
const search = await driver.waitForElementByAccessibilityId('Discovery Search Input', wd.asserters.isDisplayed, 10000);
await search.type('mye2epost');
await sleep(4000);
// activity menu button
const activityMenu = await driver.waitForElementByAccessibilityId('Activity Menu button', wd.asserters.isDisplayed, 5000);
await activityMenu.click();
await sleep(500);
// tap edit
const edit = await driver.waitForElementByAndroidUIAutomator('new UiSelector().text("Edit")', wd.asserters.isDisplayed, 5000);
await edit.click();
// change the text
const editorInput = await driver.waitForElementByAccessibilityId('Post editor input', wd.asserters.isDisplayed, 5000);
await editorInput.type(' edited!');
// tap save
const save = await driver.waitForElementByAccessibilityId('Post editor save button', wd.asserters.isDisplayed, 5000);
await save.click();
// confirm activity text changed
await driver.waitForElementByAndroidUIAutomator('new UiSelector().text("My e2e post #mye2epost edited!")', wd.asserters.isDisplayed, 5000);
});
});
\ No newline at end of file
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import { driver, capabilities} from './config';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
describe('Login flow', () => {
let username, password, loginButton;
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
});
afterAll(async () => {
await driver.quit();
});
it('should shows login error on wrong credentials', async () => {
expect(await driver.hasElementByAccessibilityId('username input')).toBe(true);
expect(await driver.hasElementByAccessibilityId('password input')).toBe(true);
username = await driver.elementByAccessibilityId('username input');
await username.type('myuser');
password = await driver.elementByAccessibilityId('password input');
await password.type('mypass');
loginButton = await driver.elementByAccessibilityId('login button');
await loginButton.click();
// message should appear
await driver.waitForElementByAccessibilityId('loginMsg', wd.asserters.isDisplayed, 5000);
const textElement = await driver.elementByAccessibilityId('loginMsg');
expect(await textElement.text()).toBe('The user credentials were incorrect.');
});
it('should go to newsfeed on successful login', async () => {
// try successfull login
await username.type(process.env.loginUser);
await password.type(process.env.loginPass);
await loginButton.click();
// should open the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 5000);
});
it('should go to login after logout', async () => {
const menu = await driver.elementByAccessibilityId('Main menu button');
// tap menu button
await menu.click();
const logout = await driver.waitForElementByAccessibilityId('Logout', wd.asserters.isDisplayed, 5000);
// tap logout
logout.click();
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
})
});
\ No newline at end of file
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import post from './actions/post';
import login from './actions/login';
import { driver, capabilities} from './config';
import sleep from '../src/common/helpers/sleep';
import pressCapture from './actions/pressCapture';
import acceptPermissions from './actions/acceptPermissions';
import attachPostGalleryImage from './actions/attachPostGalleryImage';
import selectNsfw from './actions/selectNsfw';
import lockPost from './actions/lockPost';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
//TODO: add support for ios to this test (xpath)
describe('Post flow tests', () => {
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
// we should login
await login(driver);
});
afterAll(async () => {
await driver.quit();
});
it('should post a text and see it in the newsfeed', async () => {
const str = 'My e2e post #mye2epost';
// press capture button
await pressCapture(driver);
// accept gallery permissions
await acceptPermissions(driver);
// make the post
await post(driver, str);
// should post and return to the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 10000);
// the first element of the list should be the post
const textElement = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Newsfeed Screen"]/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[2]/android.view.ViewGroup/android.widget.TextView[2]');
expect(await textElement.text()).toBe(str);
});
it('should remind the previuos post and see it in the newsfeed', async () => {
const str = 'Reminding my own post';
const remindButton = await driver.waitForElementByAccessibilityId('Remind activity button', 5000);
// tap remind
await remindButton.click();
// make the post
await post(driver, str);
// should post and return to the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 10000);
// the first element of the list should be the post
const textElement = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Newsfeed Screen"]/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[2]/android.view.ViewGroup/android.widget.TextView[2]');
expect(await textElement.text()).toBe(str);
});
it('should post a nsfw and see it in the newsfeed', async () => {
const str = 'My e2e post #mye2epost';
// press capture button
await pressCapture(driver);
// select nsfw
await selectNsfw(driver, ['Nudity', 'Pornography']);
// make the post
await post(driver, str);
// should post and return to the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 10000);
// the first element of the list should be the post
const textElement = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Newsfeed Screen"]/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[2]/android.view.ViewGroup/android.widget.TextView[2]');
expect(await textElement.text()).toBe(str);
});
it('should post paywalled content', async () => {
// press capture button
await pressCapture(driver);
// deselect nsfw
await selectNsfw(driver, ['Nudity', 'Pornography']);
const str = 'pay me something';
await lockPost(driver, '1');
// make the post with image and no permissions wait
await post(driver, str);
await sleep(1000);
const textElement = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Newsfeed Screen"]/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[2]/android.view.ViewGroup/android.widget.TextView[3]');
expect(await textElement.text()).toBe('Locked');
});
it('should post an image and see it in the newsfeed', async () => {
const str = 'My e2e post image #mye2epostimage';
// press capture button
await pressCapture(driver);
// attach image
await attachPostGalleryImage(driver);
// make the post with image and no permissions wait
await post(driver, str);
// should post and return to the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 10000);
// the first element of the list should be the post
const textElement = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Newsfeed Screen"]/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[2]/android.view.ViewGroup/android.widget.TextView[2]');
expect(await textElement.text()).toBe(str);
});
it('should open the images in full screen after tap', async () => {
// get the Image touchable
const imageButton = await driver.waitForElementByAccessibilityId('Posted Image', wd.asserters.isDisplayed, 10000);
await imageButton.click();
const image = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.widget.ImageView');
const goBack = await driver.waitForElementByAccessibilityId('Go back button', wd.asserters.isDisplayed, 10000);
await goBack.click();
});
it('should upload an image and cancel it', async () => {
// press capture button
await pressCapture(driver);
// attach image
await attachPostGalleryImage(driver);
await sleep(5000);
// we press post button
const deleteButton = await driver.elementByAccessibilityId('Attachment Delete Button');
await deleteButton.click();
await sleep(1000);
// should fail to find the delete button
return expect(driver.elementByAccessibilityId('Attachment Delete Button')).rejects.toHaveProperty('status', 7);
});
it('should return to the newsfeed', async () => {
// tap the back button
await driver.back();
// should open the newsfeed
await driver.waitForElementByAccessibilityId('Newsfeed Screen', wd.asserters.isDisplayed, 5000);
});
});
\ No newline at end of file
import wd from 'wd';
import reporterFactory from '../tests-helpers/browserstack-reporter.factory';
import login from './actions/login';
import { driver, capabilities} from './config';
import sleep from '../src/common/helpers/sleep';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const data = {sessiondID: null};
jasmine.getEnv().addReporter(reporterFactory(data));
//TODO: add support for ios to this test (xpath)
describe('Top-bar tests', () => {
beforeAll(async () => {
await driver.init(capabilities);
data.sessiondID = await driver.getSessionId();
console.log('BROWSERSTACK_SESSION: ' + data.sessiondID);
await driver.waitForElementByAccessibilityId('username input', wd.asserters.isDisplayed, 5000);
// we should login
await login(driver);
});
afterAll(async () => {
await driver.quit();
});
it('should open the boost console', async () => {
const button = await driver.waitForElementByAccessibilityId('boost-console button', wd.asserters.isDisplayed, 7000);
await button.click();
const textElement = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[2]/android.widget.TextView');
expect(await textElement.text()).toBe('Boost Console');
const back = await driver.waitForElementByXPath('//android.widget.Button[@content-desc="Go back"]/android.view.ViewGroup/android.widget.ImageView');
back.click();
});
it('should open the users profile on clicking the profile avatar', async () => {
const button = await driver.waitForElementByAccessibilityId('topbar avatar button', wd.asserters.isDisplayed, 5000);
await button.click();
await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[3]/android.widget.ImageView');
const back = await driver.waitForElementByXPath('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup/android.view.ViewGroup[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[2]/android.widget.TextView'); back.click();
});
it('should open the menu when clicking the hamburger menu', async () => {
const button = await driver.waitForElementByAccessibilityId('Main menu button', wd.asserters.isDisplayed, 5000);
await button.click();
const logoutButton = await driver.waitForElementByXPath('//android.view.ViewGroup[@content-desc="Logout"]/android.widget.TextView[2]');
expect(await logoutButton.text()).toBe('Logout');
const back = await driver.waitForElementByXPath('//android.widget.Button[@content-desc="Go back"]/android.view.ViewGroup/android.widget.ImageView');
back.click();
});
});
......@@ -35,7 +35,7 @@ exports[`LoginForm component should renders correctly 1`] = `
},
]
}
testID="username input"
testID="usernameInput"
underlineColorAndroid="transparent"
value=""
/>
......@@ -67,7 +67,7 @@ exports[`LoginForm component should renders correctly 1`] = `
},
]
}
testID="password input"
testID="userPasswordInput"
underlineColorAndroid="transparent"
value=""
/>
......@@ -251,7 +251,7 @@ exports[`LoginForm component should renders correctly 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={null}
testID="login button"
testID="loginButton"
>
<View
pointerEvents="box-only"
......
/**
* Login action
* @param {string} username
* @param {string} password
*/
export default async function(username, password) {
await element(by.id('usernameInput')).typeText(username);
await element(by.id('userPasswordInput')).typeText(password);
await element(by.id('loginButton')).tap();
}
{
"setupFilesAfterEnv": ["./init.js"],
"testEnvironment": "node",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}
const detox = require('detox');
const config = require('../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');
const assignReporter = require('detox/runners/jest/assignReporter');
// 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);
// This will post which device has assigned to run a suite, which can be useful in a multiple-worker tests run.
// This is strictly optional.
jasmine.getEnv().addReporter(assignReporter);
beforeAll(async () => {
await detox.init(config, { launchApp: false });
});
beforeEach(async () => {
await adapter.beforeEach();
});
afterAll(async () => {
await adapter.afterAll();
await detox.cleanup();
});