...
 
Commits (526)
version: 2
jobs:
test:
working_directory: ~/mobile-native
docker:
- image: circleci/node:10
steps:
- checkout
- restore_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
- restore_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}
- run: yarn install
- save_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
paths:
- ~/.cache/yarn
- save_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}
paths:
- node_modules
- run:
name: jest tests
command: |
mkdir -p test-results/jest
yarn run test --maxWorkers=2
environment:
JEST_JUNIT_OUTPUT: test-results/jest/junit.xml
- persist_to_workspace:
root: ~/mobile-native
paths:
- node_modules
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
ios:
macos:
xcode: "11.1.0"
working_directory: ~/mobile-native
# use a --login shell so our "set Ruby version" command gets picked up for later steps
shell: /bin/bash --login -o pipefail
steps:
- checkout
- run:
name: Install sentry cli for fastlane plugin
command: brew install getsentry/tools/sentry-cli
- run:
name: set Ruby version
command: echo "ruby-2.6" > ~/.ruby-version
- restore_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
- 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 --frozen-lockfile
- save_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
paths:
- ~/.cache/yarn
- save_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}
paths:
- node_modules
- restore_cache:
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}-{{ arch }}
- run:
command: gem update --system && gem install bundler && bundle install
working_directory: ios
- save_cache:
key: bundle-v1-{{ checksum "ios/Gemfile.lock" }}-{{ arch }}
paths:
- vendor/bundle
- restore_cache:
key: pods-v1-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
- run:
name: Install CocoaPods
command: pod install --verbose
working_directory: ios
- save_cache:
key: pods-v1-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
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
#- run:
# command: bundle exec fastlane test
# working_directory: ios
#- run:
# name: set up test results
# working_directory: ios
# when: always
# command: |
# mkdir -p test-results/fastlane test-results/xcode
# mv fastlane/report.xml test-results/fastlane
# mv fastlane/test_output/report.junit test-results/xcode/junit.xml
#- store_test_results:
# path: ios/test-results
#- store_artifacts:
# path: ios/test-results
- run:
name: Build release .ipa
command: fastlane buildrelease
working_directory: ios
branches:
only:
- /stable-*/
- /release-*/
- test/circle-ci
- run:
name: Upload to crashalytics
command: echo "TODO"
working_directory: ios
branches:
only:
- /release-*/
- run:
name: Prepare sentry release
command: fastlane preparesentry
working_directory: ios
- persist_to_workspace:
root: ~/mobile-native/ios
paths:
- version
- run:
name: Upload to Testflight release
command: fastlane testflight
working_directory: ios
sentry:
docker:
- image: getsentry/sentry-cli
working_directory: ~/mobile-native
steps:
- attach_workspace:
at: /tmp/workspace
- run:
name: Install git
command: |
apk add git
- checkout
- run:
name: Tag sentry release
command: |
version=`cat /tmp/workspace/version`
echo Tagging release with ${version}
ls -a
# release created by fastlane preparesentry
sentry-cli releases set-commits --commit "Minds / Minds Mobile@${CIRCLE_SHA1}" ${version} --log-level=debug
sentry-cli releases finalize ${version}
workflows:
version: 2
node-ios:
jobs:
- test
- ios:
requires:
- test
filters:
branches:
only:
- /stable-*/
- /test-*/
- test/circle-ci
- sentry:
requires:
- ios
module.exports = {
"parser": "babel-eslint",
"plugins": [
"react-native",
"flowtype"
],
"extends": ["plugin:react-native/all"],
"rules": {
"flowtype/boolean-style": [
2,
"boolean"
],
"flowtype/define-flow-type": 1,
"flowtype/delimiter-dangle": [
2,
"never"
],
"flowtype/generic-spacing": [
2,
"never"
],
"flowtype/no-mixed": 0,
"flowtype/no-primitive-constructor-types": 2,
"flowtype/no-types-missing-file-annotation": 2,
"flowtype/no-weak-types": 0,
"flowtype/object-type-delimiter": [
2,
"comma"
],
"flowtype/require-parameter-type": 2,
"flowtype/require-readonly-react-props": 0,
"flowtype/require-return-type": [
2,
"always",
{
"annotateUndefined": "never"
}
],
"flowtype/require-valid-file-annotation": 2,
"flowtype/semi": [
2,
"always"
],
"flowtype/space-after-type-colon": [
2,
"always"
],
"flowtype/space-before-generic-bracket": [
2,
"never"
],
"flowtype/space-before-type-colon": [
2,
"never"
],
"flowtype/type-id-match": [
2,
"^([A-Z][a-z0-9]+)+Type$"
],
"flowtype/union-intersection-spacing": [
2,
"always"
],
"flowtype/use-flow-type": 1,
"flowtype/valid-syntax": 1
},
"settings": {
"flowtype": {
"onlyFilesWithFlowAnnotation": true
}
}
}
\ No newline at end of file
root: true,
extends: '@react-native-community',
};
\ No newline at end of file
......@@ -5,26 +5,24 @@
; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
; Ignore polyfills
node_modules/react-native/Libraries/polyfills/.*
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
.*/Libraries/react-native/React.js
; These should not be required directly
; require from fbjs/lib instead: require('fbjs/lib/warning')
node_modules/warning/.*
; Ignore polyfills
.*/Libraries/polyfills/.*
; Flow doesn't support platforms
.*/Libraries/Utilities/LoadingView.js
; Ignore metro
.*/node_modules/metro/.*
[untyped]
.*/node_modules/@react-native-community/cli/.*/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow-github/
[options]
emoji=true
......@@ -32,39 +30,46 @@ emoji=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.system=haste
module.system.haste.use_name_reducers=true
# get basename
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
# strip .js or .js.flow suffix
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
# strip .ios suffix
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
module.system.haste.paths.blacklist=.*/__tests__/.*
module.system.haste.paths.blacklist=.*/__mocks__/.*
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
module.file_ext=.js
module.file_ext=.json
module.file_ext=.ios.js
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js
module.name_mapper='^react-native$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/react-native/react-native-implementation'
module.name_mapper='^react-native/\(.*\)$' -> '<PROJECT_ROOT>/node_modules/react-native/\1'
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/Image/RelativeImageStub'
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[lints]
sketchy-null-number=warn
sketchy-null-mixed=warn
sketchy-number=warn
untyped-type-import=warn
nonstrict-import=warn
deprecated-type=warn
unsafe-getters-setters=warn
inexact-spread=warn
unnecessary-invariant=warn
signature-verification-failure=warn
deprecated-utility=error
[strict]
deprecated-type
nonstrict-import
sketchy-null
unclear-type
unsafe-getters-setters
untyped-import
untyped-type-import
[version]
^0.92.0
^0.107.0
#!/bin/sh
if git commit -v --dry-run | grep '!testcode' >/dev/null 2>&1
then
echo "Trying to commit test code."
exit 1
else
if git diff --cached locales/en.json | grep '\\n' >/dev/null 2>&1
then
echo "New line characters are forbiden in en.json, please split the lines in different translation terms."
exit 1
else
exit 0
fi
fi
\ No newline at end of file
......@@ -5,7 +5,6 @@
# Xcode
#
ios/Podfile.lock
build/
*.pbxuser
!default.pbxuser
......@@ -42,6 +41,7 @@ yarn-error.log
buck-out/
\.buckd/
*.keystore
!debug.keystore
# fastlane
#
......@@ -57,7 +57,15 @@ buck-out/
# Bundle artifact
*.jsbundle
# CocoaPods
/ios/Pods/
# Jest cache
.jest/
coverage/
\ No newline at end of file
coverage/
# Sentry secrets
sentry.properties
!/.githooks
\ No newline at end of file
......@@ -2,73 +2,174 @@
stages:
- test
- build
- test_e2e
- i18n
- deploy
variables:
GRADLE_USER_HOME: $CI_PROJECT_DIR/.gradle
test_spec:
image: node:10.10.0
# Spec test
test:jest:
image: node:10.16.3
stage: test
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .jest/cache/
before_script:
- yarn install --frozen-lockfile
script:
- echo $CI_PROJECT_NAMESPACE
- yarn install
- yarn test
tags:
- docker
build:
image: reactnativecommunity/react-native-android
stage: build
# Upload new terms to poeditor
i18n:upload:
image: node:10.16.3
stage: i18n
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .gradle/wrapper
- .gradle/caches
- node_modules/
before_script:
- echo 999999 > /proc/sys/fs/inotify/max_user_instances
- echo 999999 > /proc/sys/fs/inotify/max_user_watches
- echo 999999 > /proc/sys/fs/inotify/max_queued_events
- echo $ANDROID_KEYSTORE | base64 --decode > android/app/minds.keystore
- echo "MYAPP_RELEASE_STORE_PASSWORD=${KEYSTORE_PASSWORD}" >> ./android/gradle.properties
- echo "MYAPP_RELEASE_KEY_PASSWORD=${KEYSTORE_PASSWORD}" >> ./android/gradle.properties
- yarn install --frozen-lockfile
script:
- yarn locale upload --poeditor-key=${CI_POEDITOR_KEY} --overwrite=1
only:
refs:
- /^release-*/
# Upload new terms and remove the deleted
i18n:uploadsync:
image: node:10.16.3
stage: i18n
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- yarn install --frozen-lockfile
script:
- yarn install
- cd android && chmod +x gradlew
- ./gradlew assembleRelease
- cp app/build/outputs/apk/release/app-release.apk ../Minds.apk
- yarn locale upload --poeditor-key=${CI_POEDITOR_KEY} --overwrite=1 --sync_terms=1
only:
refs:
- master
# Web dev version using cache and without sentry maps upload
build:android:
image: circleci/android:api-28-node
stage: build
cache:
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
paths:
- node_modules/
- android/vendor/bundle
- .gradle/caches
- .gradle/wrapper
- .android/build-cache/
before_script:
- 'sed -i ''s/^apply from: "..\/..\/node_modules\/\@sentry\/react-native\/sentry.gradle"//'' android/app/build.gradle'
- export ANDROID_SDK_HOME=$CI_PROJECT_DIR
- export GRADLE_USER_HOME="$CI_PROJECT_DIR/.gradle"
- sudo sysctl fs.inotify.max_user_watches=524288
- sudo sysctl -p
- yarn install --frozen-lockfile
- cd android
- bundle install --path=vendor/bundle
script:
- bundle exec fastlane assemble_build
- mv app/build/outputs/apk/release/app-release.apk ../Minds-$CI_COMMIT_REF_SLUG-dev.apk
artifacts:
name: "Minds APK"
paths:
- Minds.apk
- Minds-$CI_COMMIT_REF_SLUG-dev.apk
expire_in: 7 days
when: on_success
only:
variables:
- $CI_PROJECT_NAMESPACE == "minds"
refs:
- /^release-*/
# Web version (Higher version code)
build:androidproduction:
image: circleci/android:api-28-node
stage: build
before_script:
- export ANDROID_SDK_HOME=$CI_PROJECT_DIR
- export GRADLE_USER_HOME="$CI_PROJECT_DIR/.gradle"
- sudo sysctl fs.inotify.max_user_watches=524288
- sudo sysctl -p
- yarn install --frozen-lockfile
- cd android
- bundle install --path=vendor/bundle
script:
- bundle exec fastlane assemble_build
- mv app/build/outputs/apk/release/app-release.apk ../Minds-$CI_COMMIT_REF_SLUG.apk
artifacts:
name: "Minds APK"
paths:
- Minds-$CI_COMMIT_REF_SLUG.apk
expire_in: 7 days
when: on_success
only:
refs:
- /^stable-*/
- /^test-*/
test_e2e:
image: node:10.10.0
stage: test_e2e
cache:
# Play store version (Lowest version code)
build:androidproduction-playstore:
image: circleci/android:api-28-node
stage: build
before_script:
- 'sed -i ''s/^versionCode=/# versionCode=/'' android/gradle.properties'
- 'sed -i ''s/^## versionCode/versionCode/'' android/gradle.properties'
- export ANDROID_SDK_HOME=$CI_PROJECT_DIR
- export GRADLE_USER_HOME="$CI_PROJECT_DIR/.gradle"
- sudo sysctl fs.inotify.max_user_watches=524288
- sudo sysctl -p
- yarn install --frozen-lockfile
- cd android
- bundle install --path=vendor/bundle
script:
- bundle exec fastlane assemble_build
- mv app/build/outputs/apk/release/app-release.apk ../Minds-$CI_COMMIT_REF_SLUG-play_store.apk
artifacts:
name: "Minds APK"
paths:
- node_modules/
- .jest/cache/
- Minds-$CI_COMMIT_REF_SLUG-play_store.apk
expire_in: 7 days
when: on_success
only:
refs:
- /^stable-*/
- /^test-*/
# Deploy Web/PlayStore versions to s3 and browserstack
deploy:s3andbrowserstack:
image: minds/ci:latest
stage: deploy
script:
- echo "Upload Minds-$CI_COMMIT_REF_SLUG.apk"
- aws s3 cp Minds-$CI_COMMIT_REF_SLUG.apk s3://minds-repo/mobile/Minds-$CI_COMMIT_REF_SLUG.apk
- aws s3 cp Minds-$CI_COMMIT_REF_SLUG-play_store.apk s3://minds-repo/mobile/Minds-$CI_COMMIT_REF_SLUG-play_store.apk
- curl -u $CI_BROWSERSTACK_APIKEY -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@Minds-$CI_COMMIT_REF_SLUG.apk"
- curl -u $CI_BROWSERSTACK_APIKEY -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@Minds-$CI_COMMIT_REF_SLUG-play_store.apk"
dependencies:
- build:androidproduction
- build:androidproduction-playstore
only:
refs:
- /^stable-*/
- /^test-*/
deploy:google_play:
image: circleci/android:api-28-node
stage: deploy
before_script:
- cd android
- bundle install --path=vendor/bundle
- 'echo $ANDROID_PLAYSTORE_JSON | base64 --decode > app/play-store.json'
script:
- yarn install
- export bsAPP=`curl -u "${bsUSER}:${bsKEY}" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@./Minds.apk"| grep -o 'bs\:\/\/.*"' | sed 's/.$//'`
- yarn e2e
tags:
- docker
- echo "Upload to the play store Minds-$CI_COMMIT_REF_SLUG-play_store.apk"
- bundle exec fastlane supply --apk ../Minds-$CI_COMMIT_REF_SLUG-play_store.apk --track beta
dependencies:
- build
- build:androidproduction-playstore
only:
variables:
- $CI_PROJECT_NAMESPACE == "minds"
refs:
- /^stable-*/
- /^test-*/
module.exports = {
bracketSpacing: false,
jsxBracketSameLine: true,
singleQuote: true,
trailingComma: 'all',
};
......@@ -5,21 +5,17 @@
* @format
* @flow
*/
import './global';
import './shim'
import crypto from "crypto"; // DO NOT REMOVE!
import codePush from "react-native-code-push"; // For auto updates
import React, {
Component
Component,
} from 'react';
import {
Observer,
Provider,
} from 'mobx-react/native' // import from mobx-react/native instead of mobx-react fix test
} from 'mobx-react/native'; // import from mobx-react/native instead of mobx-react fix test
import NavigationService from './src/navigation/NavigationService';
import RNBootSplash from "react-native-bootsplash";
import {
BackHandler,
......@@ -29,10 +25,10 @@ import {
Text,
Alert,
Clipboard,
StatusBar,
} from 'react-native';
import FlashMessage from "react-native-flash-message";
import CookieManager from 'react-native-cookies';
import FlashMessage from 'react-native-flash-message';
import KeychainModalScreen from './src/keychain/KeychainModalScreen';
import BlockchainTransactionModalScreen from './src/blockchain/transaction-modal/BlockchainTransactionModalScreen';
......@@ -48,9 +44,9 @@ import sessionService from './src/common/services/session.service';
import deeplinkService from './src/common/services/deeplinks-router.service';
import badgeService from './src/common/services/badge.service';
import authService from './src/auth/AuthService';
import NotificationsService from "./src/notifications/NotificationsService";
import NotificationsService from './src/notifications/NotificationsService';
import getMaches from './src/common/helpers/getMatches';
import {CODE_PUSH_TOKEN, GOOGLE_PLAY_STORE} from './src/config/Config';
import { GOOGLE_PLAY_STORE } from './src/config/Config';
import updateService from './src/common/services/update.service';
import ErrorBoundary from './src/common/components/ErrorBoundary';
import { CommonStyle as CS } from './src/styles/Common';
......@@ -63,23 +59,36 @@ import feedsStorage from './src/common/services/sql/feeds.storage';
import connectivityService from './src/common/services/connectivity.service';
import sqliteStorageProviderService from './src/common/services/sqlite-storage-provider.service';
import commentStorageService from './src/comments/CommentStorageService';
import * as Sentry from '@sentry/react-native';
import apiService from './src/common/services/api.service';
import boostedContentService from './src/common/services/boosted-content.service';
import translationService from './src/common/services/translation.service';
let deepLinkUrl = '';
const statusBarStyle = Platform.OS === 'ios' ? 'dark-content' : 'default';
// init push service
pushService.init();
// fire sqlite init
sqliteStorageProviderService.get();
CookieManager.clearAll();
apiService.clearCookies();
// On app login (runs if the user login or if it is already logged in)
sessionService.onLogin(async () => {
const user = sessionService.getUser();
Sentry.configureScope(scope => {
scope.setUser({id: user.guid});
});
logService.info('[App] Getting minds settings and onboarding progress');
// load minds settings and onboarding progresss on login
const results = await Promise.all([mindsService.getSettings(), stores.onboarding.getProgress()]);
// load minds settings and boosted content
await Promise.all([mindsService.getSettings(), boostedContentService.load()]);
logService.info('[App] updatting features');
// reload fatures on login
......@@ -89,15 +98,15 @@ sessionService.onLogin(async () => {
pushService.registerToken();
// get onboarding progress
const onboarding = results[1];
logService.info('[App] navigating to initial screen', sessionService.initialScreen);
if (onboarding && onboarding.show_onboarding) {
sessionService.setInitialScreen('OnboardingScreen');
}
// hide splash
RNBootSplash.hide({ duration: 250 });
logService.info('[App] navigating to initial screen', sessionService.initialScreen);
NavigationService.reset(sessionService.initialScreen);
NavigationService.navigate(sessionService.initialScreen);
// check onboarding progress and navigate if necessary
stores.onboarding.getProgress();
// check update
if (Platform.OS !== 'ios' && !GOOGLE_PLAY_STORE) {
......@@ -138,15 +147,12 @@ sessionService.onLogout(() => {
// clear app badge
badgeService.setUnreadConversations(0);
badgeService.setUnreadNotifications(0);
// clear minds settings
mindsService.clear();
// clear offline cache
entitiesStorage.removeAll();
feedsStorage.removeAll();
stores.notifications.clearLocal();
stores.groupsBar.clearLocal();
translationService.purgeLanguagesCache();
});
// disable yellow boxes
......@@ -163,7 +169,6 @@ type Props = {
/**
* App
*/
@codePush
export default class App extends Component<Props, State> {
state = {
......@@ -182,10 +187,14 @@ export default class App extends Component<Props, State> {
}
/**
* On component will mount
* contructor
*/
componentWillMount() {
if (!Text.defaultProps) Text.defaultProps = {};
constructor(props) {
super(props);
if (!Text.defaultProps) {
Text.defaultProps = {};
}
Text.defaultProps.style = {
fontFamily: 'Roboto',
color: '#444',
......@@ -197,8 +206,9 @@ export default class App extends Component<Props, State> {
*/
async componentDidMount() {
try {
// load app setting before start
const results = await Promise.all([settingsStore.init(), await Linking.getInitialURL()]),
const results = await Promise.all([settingsStore.init(), await Linking.getInitialURL()]);
deepLinkUrl = results[1];
......@@ -208,17 +218,17 @@ export default class App extends Component<Props, State> {
if (!this.handlePasswordResetDeepLink()) {
logService.info('[App] initializing session');
const token = await sessionService.init();
if (!token) {
logService.info('[App] there is no active session');
NavigationService.reset('Login');
RNBootSplash.hide({ duration: 250 });
NavigationService.navigate('Login');
} else {
logService.info('[App] session initialized');
}
}
await this.checkForUpdates();
} catch(err) {
logService.exception('[App] Error initializing the app', err);
Alert.alert(
......@@ -283,21 +293,6 @@ export default class App extends Component<Props, State> {
}
}
async checkForUpdates() {
try {
const params = {
updateDialog: Platform.OS !== 'ios',
installMode: codePush.InstallMode.ON_APP_RESUME,
};
if (CODE_PUSH_TOKEN) params.deploymentKey = CODE_PUSH_TOKEN;
let response = await codePush.sync(params);
} catch (err) {
logService.exception('[App] Error checking for code push updated', err);
}
}
/**
* Render
*/
......@@ -305,13 +300,13 @@ export default class App extends Component<Props, State> {
const app = (
<Provider key="app" {...stores}>
<ErrorBoundary message="An error occurred" containerStyle={CS.centered}>
<StatusBar barStyle={statusBarStyle} />
<NavigationStack
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
<FlashMessage renderCustomContent={this.renderNotification}
/>
<FlashMessage renderCustomContent={this.renderNotification} />
</ErrorBoundary>
</Provider>
);
......@@ -325,7 +320,7 @@ export default class App extends Component<Props, State> {
);
const tosModal = (
<TosModal user={stores.user}/>
<TosModal user={stores.user} key="tosModal"/>
)
return [ app, keychainModal, blockchainTransactionModal, tosModal];
......
import {
Alert,
} from 'react-native';
import {Alert} from 'react-native';
import {
setNativeExceptionHandler,
setJSExceptionHandler
setJSExceptionHandler,
} from 'react-native-exception-handler';
import { onError } from "mobx-react";
import {onError} from 'mobx-react';
import logService from './src/common/services/log.service';
import * as Sentry from '@sentry/react-native';
import shouldReportToSentry from './src/common/helpers/errors';
// Init Sentry (if not running test)
if (process.env.JEST_WORKER_ID === undefined) {
Sentry.init({
dsn: 'https://16c9b543563140a0936cc3cd3714481d@sentry.io/1766867',
ignoreErrors: [
'Non-Error exception captured with keys: code, domain, localizedDescription', // ignore initial error of sdk
],
beforeSend(event, hint) {
if (hint.originalException) {
if (!shouldReportToSentry(hint.originalException)) {
return null;
}
}
// for dev only log into the console
if (__DEV__) {
console.log('sentry', event, hint);
return null;
}
return event;
},
});
}
// Log Mobx global errors
onError(error => {
console.log(error);
logService.exception(error);
})
// react-native-exception-handler global handlers
if (!__DEV__) {
/**
* Globar error handlers
......@@ -48,7 +75,9 @@ if (!__DEV__) {
* Native Errors
*/
setNativeExceptionHandler((exceptionString) => {
Sentry.captureException(new Error(exceptionString), {
logger: 'NativeExceptionHandler',
});
console.log(exceptionString);
logService.exception(exceptionString);
});
}
......@@ -19,8 +19,6 @@ import keychain from './src/keychain/KeychainStore';
import blockchainTransaction from './src/blockchain/transaction-modal/BlockchainTransactionStore';
import blockchainWallet from './src/blockchain/wallet/BlockchainWalletStore';
import blockchainWalletSelector from './src/blockchain/wallet/BlockchainWalletSelectorStore';
import payments from './src/payments/PaymentsStore';
import checkoutModal from './src/payments/checkout/CheckoutModalStore';
import capture from './src/capture/CaptureStore';
import withdraw from './src/wallet/tokens/WithdrawStore';
import hashtag from './src/common/stores/HashtagStore';
......@@ -29,11 +27,13 @@ import groupsBar from './src/groups/GroupsBarStore';
import sessionService from './src/common/services/session.service';
import logService from './src/common/services/log.service';
import SubscriptionRequestStore from './src/channel/subscription/SubscriptionRequestStore';
/**
* App stores
*/
const stores = {
subscriptionRequest: new SubscriptionRequestStore(),
newsfeed: new newsfeed(),
notifications: new notifications(),
notificationsSettings: new notificationsSettings(),
......@@ -55,8 +55,6 @@ const stores = {
blockchainWallet: new blockchainWallet(),
blockchainWalletSelector: new blockchainWalletSelector(),
channelSubscribersStore: new channelSubscribersStore(),
payments: new payments(),
checkoutModal: new checkoutModal(),
capture: new capture(),
withdraw: new withdraw(),
hashtag: new hashtag(),
......
# Minds Mobile Apps
## Branch Structure
| Branch | |
|-----------|------------------------------------------------------------------------------------------------------------------------------------|
| master | Approved code ready to merged into the next stable release. All tests should pass and be in a 'ready' state |
| stable/* | Stable builds, inherited from `release/*` branches. Fastlane automatically deploys these builds. |
| release/* | WIP builds. Run `fastlane run increment_version_number` upon creating the branch. |
| feat/* | New branches should be made for each Gitlab issue. Merge requests should be opened pointing towards the respective release branch. |
## Increasing the version number
### Patch
`fastlane run increment_version_number bump_type:patch`
### Minor
`fastlane run increment_version_number bump_type:minor`
### Major
`fastlane run increment_version_number bump_type:major`
## Platforms
- iOS
- Android
## Building
## Install dependencies
- `yarn install`
- `react-native run-ios` or `react-native run-android`
- `cd ios && pod install` (iOS only)
## Building
- `yarn android` or `yarn ios`
## Testing
- `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_
\ No newline at end of file
### _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 0', 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, 1000);
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[1]');
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.FrameLayout/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, 5000);
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[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[1]/android.widget.ScrollView/android.view.ViewGroup/android.view.ViewGroup[1]/android.view.ViewGroup[1]/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[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();
});
});
function load(count) {
function load(count, container) {
let activities = [...Array(count)].map((_, i) => {
const code = 'activityguid' + i;
......@@ -14,16 +14,17 @@ function load(count) {
edited:"",
guid:code,
mature:false,
time_created: "1522036284",
ownerObj:{
guid: "824853017709780997",
type: "user",
subtype: false,
time_created: "1522036284",
getAvatarSource: () => {
return {
source:'http://thisisaurl'
}
}
// getAvatarSource: () => {
// return {
// source:'http://thisisaurl'
// }
// }
},
shouldBeBlured: jest.fn(),
message:"Message",
......@@ -36,11 +37,20 @@ function load(count) {
wire_totals: {
tokens: 1000000000000000000
},
getThumbSource: () => {
return {
source:'http://thisisaurl'
}
}
containerObj: container
// _list: {
// viewed: {
// viewed: new Map([["1019155171608096768",true]]),
// addViewed: () => {
// return;
// }
// }
// },
// getThumbSource: () => {
// return {
// source:'http://thisisaurl'
// }
// }
}
});
......
......@@ -16,6 +16,7 @@ export default function(guid, owner=userFakeFactory(100)) {
"featured_id":false,
"ownerObj": owner,
"category":"education",
"impressions": 100,
"thumbs:up:count": 2,
"thumbs:up:user_guids":["1","2"],
"thumbs:down:count":1,
......
......@@ -14,6 +14,9 @@ export default function(guid, name='Some User', username='someusermane') {
"access_id":"2",
"language":"en",
"icontime":"1523515420",
"impressions":100,
"subscribers_count": 50,
"blocked": false,
"legacy_guid":false,
"featured_id":"725165605782560768",
"banned":"no",
......
export default {
}
\ No newline at end of file
module.exports = {
getVersion: jest.fn().mockImplementation(() => '3.8.0'),
getBuildNumber: jest.fn(),
// add more methods as needed
};
\ No newline at end of file
export default {
};
\ No newline at end of file
import { View as mockView } from "react-native";
export default {
Value: jest.fn(),
event: jest.fn(),
add: jest.fn(),
eq: jest.fn(),
set: jest.fn(),
cond: jest.fn(),
interpolate: jest.fn(),
View: mockView,
Extrapolate: { CLAMP: jest.fn() },
Clock: jest.fn(),
greaterThan: jest.fn(),
lessThan: jest.fn(),
startClock: jest.fn(),
stopClock: jest.fn(),
clockRunning: jest.fn(),
not: jest.fn(),
or: jest.fn(),
and: jest.fn(),
spring: jest.fn(),
decay: jest.fn(),
defined: jest.fn(),
call: jest.fn(),
Code: mockView,
block: jest.fn(),
abs: jest.fn(),
greaterOrEq: jest.fn(),
lessOrEq: jest.fn(),
debug: jest.fn(),
Transition: {
Out: "Out"
},
};
export default {
Header: {}
};
export const createStackNavigator = jest.fn();
export const Header = {
HEIGHT:80,
};
export const createMaterialTopTabNavigator = jest.fn().mockImplementation(x => ({router: 'router'}));
export default {
createDrawerNavigator: jest.fn(),
withNavigation: jest.fn(),
StackActions: {
push: jest.fn().mockImplementation(x => ({...x, "type": "Navigation/PUSH"})),
replace: jest.fn().mockImplementation(x => ({...x, "type": "Navigation/REPLACE"})),
},
};
export const createAppContainer = jest.fn().mockReturnValue(function NavigationContainer(props) {return null;});
export const createSwitchNavigator = jest.fn().mockImplementation(x => ({router: 'router'}));
export const withNavigation = jest.fn();
export const Header = {
HEIGHT:80
}
export const StackActions = {
reset: jest.fn()
}
......
import 'react-native';
import React from 'react';
import App from '../App';
import videochat from '../src/common/services/videochat.service';
import sqliteStorageProviderService from '../src/common/services/sqlite-storage-provider.service';
import logservice from '../src/common/services/log.service';
import logService from '../src/common/services/log.service';
import {
BackHandler,
} from 'react-native';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
// mock backhandler
BackHandler.addEventListener = jest.fn();
jest.mock('../src/common/services/sqlite-storage-provider.service')
jest.mock('../src/common/services/log.service', () => {});
jest.mock('../src/common/services/push.service');
jest.mock('react-native-gesture-handler', () => {});
jest.mock('react-navigation-stack', () => { Header: {} });
jest.mock('react-navigation', () => {
return {
createAppContainer: jest.fn().mockReturnValue(function NavigationContainer(props) {return null;}),
createDrawerNavigator: jest.fn(),
createMaterialTopTabNavigator: jest.fn().mockImplementation(x => ({router: 'router'})),
createStackNavigator: jest.fn(),
withNavigation: jest.fn(),
StackActions: {
push: jest.fn().mockImplementation(x => ({...x, "type": "Navigation/PUSH"})),
replace: jest.fn().mockImplementation(x => ({...x, "type": "Navigation/REPLACE"})),
},
NavigationActions: {
navigate: jest.fn().mockImplementation(x => x),
}
}
});
// use the web3 mock to prevent sintax error from node_tar
jest.mock('web3');
//mock notifications
jest.mock('react-native-notifications');
jest.mock('../src/common/services/videochat.service');
jest.mock('react-native-convert-ph-asset');
jest.mock('react-navigation');
jest.mock('react-navigation-tabs');
jest.mock('react-navigation-stack');
//mock i18n
jest.mock('react-native-video');
......
import 'react-native';
import React from 'react';
import { shallow } from 'enzyme';
import {
FlatList,
KeyboardAvoidingView
} from 'react-native';
import ActivityScreen from '../../../src/newsfeed/ActivityScreen';
// import SingleEntityStore from '../../../src/common/stores/SingleEntityStore';
import RichEmbedStore from '../../../src/common/stores/RichEmbedStore';
import commentsStoreProvider from '../../../src/comments/CommentsStoreProvider';
import { commentsServiceFaker } from '../../../__mocks__/fake/CommentsFaker';
import { activitiesServiceFaker } from '../../../__mocks__/fake/ActivitiesFaker';
import CommentList from '../../../src/comments/CommentList';
jest.mock('../../../src/newsfeed/NewsfeedService');
import { getSingle } from '../../../src/newsfeed/NewsfeedService';
import entitiesService from '../../../src/common/services/entities.service';
import ActivityModel from '../../../src/newsfeed/ActivityModel';
jest.mock('../../../src/newsfeed/NewsfeedService');
jest.mock('../../../src/newsfeed/activity/Activity', () => 'Activity');
jest.mock('../../../src/comments/CommentList', () => 'CommentList');
jest.mock('../../../src/common/components/CenteredLoading', () => 'CenteredLoading');
jest.mock('../../../src/comments/CommentsStore');
jest.mock('../../../src/comments/CommentsStoreProvider');
jest.mock('../../../src/common/services/entities.service');
describe('Activity screen component', () => {
......@@ -50,39 +48,27 @@ describe('Activity screen component', () => {
params: {entity: activitiesServiceFaker().load(1).activities[0]}
}
};
screen = shallow(
<ActivityScreen navigation={navigation}/>
);
expect(screen).toMatchSnapshot();
// should have a comment list component
expect(screen.find(CommentList)).toHaveLength(1);
});
it('should show loader until it loads the activity', async (done) => {
navigation = {
push: jest.fn(),
state: {
routeName: 'some',
params: {guid: '1'}
}
};
getSingle.mockResolvedValue(activitiesServiceFaker().load(1).activities[0]);
entitiesService.single.mockResolvedValue(ActivityModel.create(navigation.state.params.entity));
screen = shallow(
<ActivityScreen navigation={navigation}/>
);
// shoul show loading
expect(screen).toMatchSnapshot();
screen.update();
// unmount
await screen.instance().loadEntity();
// workaround to run after the async didmount
setImmediate(() => {
// should show the activity
expect(screen).toMatchSnapshot();
done();
});
jest.runAllTicks();
// await is important here!
await screen.update();
// should show the activity
expect(screen).toMatchSnapshot();
// should have a comment list component
expect(screen.find(CommentList)).toHaveLength(1);
});
});
import 'react-native';
import React from 'react';
import { Text, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import { activitiesServiceFaker } from '../../../__mocks__/fake/ActivitiesFaker';
import renderer from 'react-test-renderer';
import OwnerBlock from '../../../src/newsfeed/activity/OwnerBlock';
import withPreventDoubleTap from '../../../src/common/components/PreventDoubleTap';
import ActivityModel from '../../../src/newsfeed/ActivityModel';
// const DebouncedTouchableOpacity = withPreventDoubleTap(TouchableOpacity);
describe('Owner component', () => {
let screen;
beforeEach(() => {
const TouchableOpacityCustom = <TouchableOpacity onPress={this.onPress} />;
const navigation = { navigate: jest.fn(), push: jest.fn() };
let activityResponse = activitiesServiceFaker().load(1);
let screen, entity, navigation;
beforeEach(() => {
navigation = { navigate: jest.fn(), push: jest.fn() };
const activityResponse = activitiesServiceFaker().load(1, {guid:1, name:'group'});
entity = ActivityModel.create(activityResponse.activities[0]);
screen = shallow(
<OwnerBlock entity ={activityResponse.activities[0]} navigation={navigation} rightToolbar={null}/>
<OwnerBlock entity ={entity} navigation={navigation} rightToolbar={null}/>
);
jest.runAllTimers();
});
it('renders correctly', async () => {
......@@ -33,58 +28,28 @@ describe('Owner component', () => {
it('should have PreventDoubleTap', async () => {
screen.update();
expect(screen.find('PreventDoubleTap')).toHaveLength(2);
expect(screen.find('PreventDoubleTap')).toHaveLength(3);
});
it('should _navToChannel on press ', () => {
let activityResponse = activitiesServiceFaker().load(1);
const navigation = {
navigate: jest.fn(),
push: jest.fn()
};
let entity = activityResponse.activities[0];
entity.containerObj = { guid: 'guidguid' };
screen = shallow(
<OwnerBlock entity={entity} navigation={navigation} rightToolbar={null}/>
);
screen.update()
let touchables = screen.find('PreventDoubleTap');
touchables.at(0).props().onPress();
jest.runAllTimers();
expect(navigation.push).toHaveBeenCalledWith('Channel', {'entity': entity.ownerObj, 'guid': entity.ownerObj.guid});
expect(screen.find('PreventDoubleTap')).toHaveLength(3);
});
it('should nav to groups on press ', () => {
let activityResponse = activitiesServiceFaker().load(1);
const navigation = {
navigate: jest.fn(),
push: jest.fn()
};
let entity = activityResponse.activities[0];
entity.containerObj = { guid: 'guidguid' };
screen = shallow(
<OwnerBlock entity={entity} navigation={navigation} rightToolbar={null}/>
);
screen.update()
let touchables = screen.find('PreventDoubleTap');
expect(screen.find('PreventDoubleTap')).toHaveLength(3);
//group touchable
touchables.at(2).props().onPress();
jest.runAllTimers();
expect(navigation.push).toHaveBeenCalled();
expect(navigation.push).toHaveBeenCalledWith('GroupView', {"group": {"guid": 1, "name": "group"}});
});
});
import 'react-native';
import React from 'react';
import { Text, TouchableOpacity } from "react-native";
import { TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import { activitiesServiceFaker } from '../../../__mocks__/fake/ActivitiesFaker';
import renderer from 'react-test-renderer';
import RemindOwnerBlock from '../../../src/newsfeed/activity/RemindOwnerBlock';
import formatDate from '../../../src/common/helpers/date';
import domain from '../../../src/common/helpers/domain';
import ActivityModel from '../../../src/newsfeed/ActivityModel';
describe('Remind owner component', () => {
let user, comments, entity, screen;
let entity, screen, navigation;
beforeEach(() => {
let activityResponse = activitiesServiceFaker().load(1);
entity = ActivityModel.create(activitiesServiceFaker().load(1).activities[0]);
navigation = {
navigate: jest.fn(),
push: jest.fn(),
};
screen = shallow(
<RemindOwnerBlock entity={activityResponse.activities[0]}/>
<RemindOwnerBlock entity={entity} navigation={navigation}/>
);
jest.runAllTimers();
});
it('renders correctly', async () => {
......@@ -36,26 +36,12 @@ describe('Remind owner component', () => {
it('should _navToChannel on press ', () => {
let activityResponse = activitiesServiceFaker().load(1);
const navigation = {
navigate: jest.fn(),
push: jest.fn(),
};
let entity = activityResponse.activities[0];
screen = shallow(
<RemindOwnerBlock entity={entity} navigation={navigation} rightToolbar={null}/>
);
screen.update()
let touchables = screen.find('TouchableOpacity');
touchables.at(0).props().onPress();
jest.runAllTimers();
expect(navigation.push).toHaveBeenCalledWith('Channel', {'entity': entity.ownerObj, 'guid': entity.ownerObj.guid});
expect(screen.find(TouchableOpacity)).toHaveLength(2);
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Activity component renders correctly 1`] = `
<ContextConsumer>
<Component />
</ContextConsumer>
<View
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"alignItems": "center",
"display": "flex",
"flexDirection": "row",
"padding": 4,
"paddingBottom": 8,
"paddingTop": 8,
}
}
>
<ThumbUpAction
entity={
Object {
"attachment_guid": false,
"blurb": false,
"containerObj": undefined,
"container_guid": "activityguid0",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"mature": false,
"message": "Message",
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"rowKey": "something0",
"shouldBeBlured": [MockFunction],
"thumbnail_src": false,
"time_created": "1522036284",
"title": "TITLE",
"type": "activity",
"wire_totals": Object {
"tokens": 1000000000000000000,
},
}
}
me={
Object {
"guid": "guidguid",
}
}
/>
<ThumbDownAction
entity={
Object {
"attachment_guid": false,
"blurb": false,
"containerObj": undefined,
"container_guid": "activityguid0",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"mature": false,
"message": "Message",
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"rowKey": "something0",
"shouldBeBlured": [MockFunction],
"thumbnail_src": false,
"time_created": "1522036284",
"title": "TITLE",
"type": "activity",
"wire_totals": Object {
"tokens": 1000000000000000000,
},
}
}
me={
Object {
"guid": "guidguid",
}
}
/>
<WireAction
owner={
Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
}
}
/>
<CommentsAction
entity={
Object {
"attachment_guid": false,
"blurb": false,
"containerObj": undefined,
"container_guid": "activityguid0",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"mature": false,
"message": "Message",
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"rowKey": "something0",
"shouldBeBlured": [MockFunction],
"thumbnail_src": false,
"time_created": "1522036284",
"title": "TITLE",
"type": "activity",
"wire_totals": Object {
"tokens": 1000000000000000000,
},
}
}
/>
<RemindAction
entity={
Object {
"attachment_guid": false,
"blurb": false,
"containerObj": undefined,
"container_guid": "activityguid0",
"custom_data": false,
"custom_type": false,
"description": "Congratulations! ",
"edited": "",
"guid": "activityguid0",
"mature": false,
"message": "Message",
"ownerObj": Object {
"guid": "824853017709780997",
"subtype": false,
"time_created": "1522036284",
"type": "user",
},
"owner_guid": "824853017709780997",
"parent_guid": "838106762591510528",
"perma_url": false,
"rowKey": "something0",
"shouldBeBlured": [MockFunction],
"thumbnail_src": false,
"time_created": "1522036284",
"title": "TITLE",
"type": "activity",
"wire_totals": Object {
"tokens": 1000000000000000000,
},
}
}
/>
</View>
</View>
`;
......@@ -70,6 +70,8 @@ exports[`Activity editor component renders correctly 1`] = `
nsfwValue={Array []}
onLocking={[Function]}
onNsfw={[Function]}
onScheduled={[Function]}
timeCreatedValue={2018-03-26T03:51:24.000Z}
/>
</View>
<View
......
......@@ -18,7 +18,12 @@ exports[`Owner component renders correctly 1`] = `
resizeMode="cover"
source={
Object {
"source": "http://thisisaurl",
"headers": Object {
"App-Version": "3.8.0",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
"uri": "https://cdn.minds.com/icon/824853017709780997/medium/undefined",
}
}
style={
......@@ -62,6 +67,30 @@ exports[`Owner component renders correctly 1`] = `
}
/>
</PreventDoubleTap>
<PreventDoubleTap
onPress={[Function]}
style={
Object {
"flex": 1,
"marginLeft": 4,
}
}
>
<Text
lineBreakMode="tail"
numberOfLines={1}
style={
Object {
"color": "#888",
"fontFamily": "Roboto",
"fontWeight": "bold",
}
}
>
&gt;
group
</Text>
</PreventDoubleTap>
</View>
</View>
</View>
......
......@@ -36,7 +36,12 @@ exports[`Remind owner component renders correctly 1`] = `
resizeMode="cover"
source={
Object {
"source": "http://thisisaurl",
"headers": Object {
"App-Version": "3.8.0",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
},
"uri": "https://cdn.minds.com/icon/824853017709780997/medium/undefined",
}
}
style={
......
import 'react-native';
import React from 'react';
import { Text, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import { activitiesServiceFaker } from '../../../../__mocks__/fake/ActivitiesFaker';
import renderer from 'react-test-renderer';
import RemindAction from '../../../../src/newsfeed/activity/actions/RemindAction';
import withPreventDoubleTap from '../../../../src/common/components/PreventDoubleTap';
import featuresService from '../../../../src/common/services/features.service';
import ActivityModel from '../../../../src/newsfeed/ActivityModel';
describe('Thumb action component', () => {
let screen;
beforeEach(() => {
const TouchableOpacityCustom = <TouchableOpacity onPress={this.onPress} />;
let screen, entity;
beforeEach(() => {
const navigation = { push: jest.fn(), state: {key:1} };
let activityResponse = activitiesServiceFaker().load(1);
entity = ActivityModel.create(activityResponse.activities[0])
screen = shallow(
<RemindAction entity={activityResponse.activities[0]} navigation={navigation} />
<RemindAction entity={entity} navigation={navigation} />
);
jest.runAllTimers();
});
it('renders correctly', async () => {
screen.update();
expect(screen).toMatchSnapshot();
});
it('should have a remind button', async () => {
screen.update();
expect(screen.find('PreventDoubleTap')).toHaveLength(1)
});
it('should navigate a thumb on press ', () => {
let activityResponse = activitiesServiceFaker().load(1);
const navigation = {
push: jest.fn(),
state: {key:1}
};
let entity = activityResponse.activities[0];
entity.toggleVote = jest.fn();
entity.votedUp = true;
entity['thumbs:up:user_guids'] = ['1'];
screen = shallow(
<RemindAction entity={entity} navigation={navigation}/>
);
screen.update()
let touchables = screen.find('PreventDoubleTap');
touchables.at(0).props().onPress();
jest.runAllTimers();
expect(navigation.push).toBeCalled();
});
});
\ No newline at end of file
......@@ -11,24 +11,24 @@ import ThumbDownAction from '../../../../src/newsfeed/activity/actions/ThumbDown
import withPreventDoubleTap from '../../../../src/common/components/PreventDoubleTap';
import featuresService from '../../../../src/common/services/features.service';
import UserStore from '../../../../src/auth/UserStore';
import ActivityModel from '../../../../src/newsfeed/ActivityModel';
jest.mock('../../../../src/auth/UserStore');
// prevent double tap in touchable
describe('Thumb action component', () => {
let screen;
let screen, entity;
beforeEach(() => {
const TouchableOpacityCustom = <TouchableOpacity onPress={this.onPress} />;
const navigation = { navigate: jest.fn() };
let activityResponse = activitiesServiceFaker().load(1);
entity = ActivityModel.create(activityResponse.activities[0]);
screen = shallow(
<ThumbDownAction entity={activityResponse.activities[0]} navigation={navigation} />
<ThumbDownAction entity={entity} navigation={navigation} />
);
jest.runAllTimers();
});
it('renders correctly', async () => {
......@@ -43,21 +43,21 @@ describe('Thumb action component', () => {
});
it('should navigate a thumb on press ', () => {
let activityResponse = activitiesServiceFaker().load(1);
const navigation = {
navigate: jest.fn()
const navigation = {
navigate: jest.fn()
};
let entity = activityResponse.activities[0];
entity.toggleVote = jest.fn();
entity.votedUp = true;
screen = shallow(
<ThumbDownAction entity={entity} navigation={navigation}/>
);
screen.update();
let touchables = screen.find('PreventDoubleTap');
touchables.at(0).props().onPress();
jest.runAllTimers();
expect(entity.toggleVote).toHaveBeenCalled();
......
......@@ -11,22 +11,24 @@ import ThumbUpAction from '../../../../src/newsfeed/activity/actions/ThumbUpActi
import withPreventDoubleTap from '../../../../src/common/components/PreventDoubleTap';
import featuresService from '../../../../src/common/services/features.service';
import UserStore from '../../../../src/auth/UserStore';
import ActivityModel from '../../../../src/newsfeed/ActivityModel';
jest.mock('../../../../src/auth/UserStore');
describe('Thumb action component', () => {
let screen;
let screen, entity;
beforeEach(() => {
const TouchableOpacityCustom = <TouchableOpacity onPress={this.onPress} />;
const navigation = { navigate: jest.fn() };
let activityResponse = activitiesServiceFaker().load(1);
entity = ActivityModel.create(activityResponse.activities[0]);
screen = shallow(
<ThumbUpAction entity={activityResponse.activities[0]} navigation={navigation} />
<ThumbUpAction entity={entity} navigation={navigation} />
);
jest.runAllTimers();
});
it('renders correctly', async () => {
......@@ -41,21 +43,20 @@ describe('Thumb action component', () => {
});
it('should navigate a thumb on press ', () => {
let activityResponse = activitiesServiceFaker().load(1);
const navigation = {
navigate: jest.fn()
const navigation = {
navigate: jest.fn()
};
let entity = activityResponse.activities[0];
entity.toggleVote = jest.fn();
entity.votedUp = true;
screen = shallow(
<ThumbUpAction entity={entity} navigation={navigation}/>
);
screen.update();
let touchables = screen.find('PreventDoubleTap');
touchables.at(0).props().onPress();
jest.runAllTimers();
expect(entity.toggleVote).toHaveBeenCalled();
......
......@@ -16,7 +16,9 @@ exports[`Boost action component renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......
......@@ -8,6 +8,12 @@ exports[`Comment action component renders correctly 1`] = `
Object {
"flex": 1,
},
Object {
"alignContent": "center",
"alignItems": "center",
"alignSelf": "center",
"justifyContent": "center",
},
Object {
"flexDirection": "row",
"justifyContent": "center",
......@@ -16,16 +22,27 @@ exports[`Comment action component renders correctly 1`] = `
}
>
<Icon
color="rgb(96, 125, 139)"
name="chat-bubble"
raised={false}
reverse={false}
reverseColor="white"
allowFontScaling={false}
name="speaker-notes-off"
size={20}
underlayColor="white"
style={
Array [
Object {
"color": "#d8d8d8",
},
Object {
"marginRight": 5,
},
]
}
/>
<Counter
size={15}
size={14}
style={
Object {
"color": "rgb(96, 125, 139)",
}
}
/>
</PreventDoubleTap>
`;
......@@ -3,25 +3,22 @@
exports[`Owner component renders correctly 1`] = `
<View
style={
Object {
"alignItems": "center",
"flexDirection": "column",
"justifyContent": "center",
}
Array [
Object {
"alignItems": "center",
"flexDirection": "column",
},
]
}
>
<Text
style={
Array [
Object {
"paddingLeft": 4,
},
Object {
"color": "#888",
"fontWeight": "800",
"color": "rgb(96, 125, 139)",
},
Object {
"fontSize": 11,
"fontSize": 15,
},
]
}
......
......@@ -8,26 +8,42 @@ exports[`Thumb action component renders correctly 1`] = `
Object {
"flex": 1,
},
Object {
"alignContent": "center",
"alignItems": "center",
"alignSelf": "center",
"justifyContent": "center",
},
Object {
"flexDirection": "row",
"justifyContent": "center",
},
null,
]
}
testID="Remind activity button"
>
<Icon
color="rgb(96, 125, 139)"
allowFontScaling={false}
name="repeat"
raised={false}
reverse={false}
reverseColor="white"
size={20}
underlayColor="white"
style={
Array [
Object {
"color": "rgb(96, 125, 139)",
},
Object {
"marginRight": 5,
},
]
}
/>
<Counter
size={15}
size={14}
style={
Object {
"color": "rgb(96, 125, 139)",
}
}
/>
</PreventDoubleTap>
`;
......@@ -23,16 +23,27 @@ exports[`Thumb action component renders correctly 1`] = `
testID="Thumb down activity button"
>
<Icon
color="rgb(96, 125, 139)"
allowFontScaling={false}
name="thumb-down"
raised={false}
reverse={false}
reverseColor="white"
size={20}
underlayColor="white"
style={
Array [
Object {
"color": "rgb(96, 125, 139)",
},
Object {
"marginRight": 5,
},
]
}
/>
<Counter
size={15}
size={14}
style={
Object {
"color": "rgb(96, 125, 139)",
}
}
testID="Thumb down count"
/>
</PreventDoubleTap>
......
......@@ -23,16 +23,27 @@ exports[`Thumb action component renders correctly 1`] = `
testID="Thumb up activity button"
>
<Icon
color="rgb(96, 125, 139)"
allowFontScaling={false}
name="thumb-up"
raised={false}
reverse={false}
reverseColor="white"
size={20}
underlayColor="white"
style={
Array [
Object {
"color": "rgb(96, 125, 139)",
},
Object {
"marginRight": 5,
},
]
}
/>
<Counter
size={15}
size={14}
style={
Object {
"color": "rgb(96, 125, 139)",
}
}
testID="Thumb up count"
/>
</PreventDoubleTap>
......
......@@ -2,19 +2,15 @@ import api from '../../src/common/services/api.service';
import session from '../../src/common/services/session.service';
import authService from '../../src/auth/AuthService';
import delay from '../../src/common/helpers/delay';
import CookieManager from 'react-native-cookies';
jest.mock('../../src/common/services/api.service');
jest.mock('../../src/common/services/session.service');
jest.mock('../../src/common/helpers/delay', () => jest.fn());
jest.mock('react-native-cookies');
describe('auth service login', () => {
beforeEach(() => {
api.post.mockClear();
session.login.mockClear();
CookieManager.clearAll.mockClear();
CookieManager.clearAll.mockResolvedValue();
delay.mockClear();
delay.mockResolvedValue();
});
......@@ -104,6 +100,22 @@ describe('auth service logout', () => {
expect(session.logout.mock.calls.length).toEqual(1);
});
it('should clear cookies on logout', async () => {
api.post.mockResolvedValue(true);
const res = await authService.logout();
// assert on the response
expect(res).toEqual(true);
// call session logout one time
expect(session.logout.mock.calls.length).toBe(1);
// should clear cookies
expect(api.clearCookies).toBeCalled();
});
it('logout returns errors', async () => {
const response = {status: 'error', error: 'some error'};
......
......@@ -5,6 +5,7 @@ import { shallow } from 'enzyme';
import LoginForm from '../../src/auth/LoginForm';
import authService from '../../src/auth/AuthService';
import TextInput from '../../src/common/components/TextInput';
jest.mock('../../src/auth/AuthService');
......@@ -35,7 +36,7 @@ describe('LoginForm component', () => {
// simulate user input
const render = wrapper.dive();
render.find('TextInput').forEach(child => {
render.find(TextInput).forEach(child => {
child.simulate('changeText', 'data');
});
......
import 'react-native';
import React from 'react';
import { Text, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import RegisterScreen from '../../src/auth/RegisterScreen';
import { NavigationActions, StackActions } from 'react-navigation';
jest.mock('../../src/auth/RegisterForm', () => 'RegisterForm');
jest.mock('../../src/common/components/VideoBackground', () => 'VideoBackground');
......@@ -35,15 +33,14 @@ describe('RegisterScreen component', () => {
it('should navigate on RegisterForm events', async () => {
const navigation = {
dispatch: jest.fn()
dispatch: jest.fn(),
navigate: jest.fn(),
};
const wrapper = shallow(
<RegisterScreen navigation={navigation}/>
);
StackActions.reset.mockReturnValue(1)
// find register form
const registerForms = wrapper.find('RegisterForm');
......@@ -53,10 +50,7 @@ describe('RegisterScreen component', () => {
// simulate onBack event
registerForms.at(0).props().onBack();
// should call navigate
expect(StackActions.reset).toBeCalledWith({"actions": [undefined], "index": 0});
// with a navigation action with route to Login
expect(NavigationActions.navigate).toBeCalledWith({"routeName": "Login"});
expect(navigation.navigate).toBeCalledWith('Login');
});
});
......@@ -7,7 +7,6 @@ import UserModel from '../../src/channel/UserModel';
jest.mock('../../src/channel/ChannelService');
jest.mock('../../src/channel/UserModel');
// mock the static create method
UserModel.create = jest.fn();
......@@ -258,8 +257,10 @@ describe('user store', () => {
const fakeUser = meFactory(1);
fakeUser.admin = true;
const entity = new UserModel(fakeUser);
// mock methods called
UserModel.create.mockReturnValue(fakeUser);
UserModel.create.mockReturnValue(entity);
try {
// set the user
......
......@@ -13,7 +13,7 @@ exports[`ForgotPassword component should renders correctly 1`] = `
style={
Array [
Object {
"color": "white",
"color": "#FFFFFF",
},
Object {
"fontSize": 14,
......@@ -87,7 +87,9 @@ exports[`ForgotPassword component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -193,7 +195,9 @@ exports[`ForgotPassword component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -271,7 +275,7 @@ exports[`ForgotPassword component should renders correctly 1`] = `
]
}
>
CONTIUE
CONTINUE
</Text>
</View>
</View>
......
......@@ -42,7 +42,11 @@ exports[`ForgotScreen component should renders correctly 1`] = `
>
<Image
resizeMode="contain"
source={1}
source={
Object {
"testUri": "../../../src/assets/logos/logo-white.png",
}
}
style={
Object {
"alignSelf": "center",
......@@ -77,7 +81,7 @@ exports[`ForgotScreen component should renders correctly 1`] = `
style={
Array [
Object {
"color": "white",
"color": "#FFFFFF",
},
Object {
"fontSize": 14,
......@@ -151,7 +155,9 @@ exports[`ForgotScreen component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -257,7 +263,9 @@ exports[`ForgotScreen component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -335,7 +343,7 @@ exports[`ForgotScreen component should renders correctly 1`] = `
]
}
>
CONTIUE
CONTINUE
</Text>
</View>
</View>
......
......@@ -12,6 +12,7 @@ exports[`LoginForm component should renders correctly 1`] = `
<TextInput
allowFontScaling={true}
autoCapitalize="none"
editable={false}
onChangeText={[Function]}
placeholder="Username"
placeholderTextColor="#444"
......@@ -34,7 +35,7 @@ exports[`LoginForm component should renders correctly 1`] = `
},
]
}
testID="username input"
testID="usernameInput"
underlineColorAndroid="transparent"
value=""
/>
......@@ -42,6 +43,7 @@ exports[`LoginForm component should renders correctly 1`] = `
<TextInput
allowFontScaling={true}
autoCapitalize="none"
editable={false}
onChangeText={[Function]}
placeholder="Password"
placeholderTextColor="#444"
......@@ -65,7 +67,7 @@ exports[`LoginForm component should renders correctly 1`] = `
},
]
}
testID="password input"
testID="userPasswordInput"
underlineColorAndroid="transparent"
value=""
/>
......@@ -131,7 +133,9 @@ exports[`LoginForm component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -237,7 +241,9 @@ exports[`LoginForm component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -245,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"
......@@ -340,7 +346,7 @@ exports[`LoginForm component should renders correctly 1`] = `
style={
Array [
Object {
"color": "white",
"color": "#FFFFFF",
},
Object {
"fontWeight": "bold",
......@@ -394,7 +400,7 @@ exports[`LoginForm component should renders correctly 1`] = `
style={
Array [
Object {
"color": "white",
"color": "#FFFFFF",
},
]
}
......@@ -411,6 +417,7 @@ exports[`LoginForm component should renders correctly 1`] = `
onModalWillHide={[Function]}
onModalWillShow={[Function]}
onRequestClose={[Function]}
scrollHorizontal={false}
scrollOffset={0}
scrollOffsetMax={0}
scrollTo={null}
......@@ -426,6 +433,8 @@ exports[`LoginForm component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -452,6 +461,7 @@ exports[`LoginForm component should renders correctly 1`] = `
onModalWillHide={[Function]}
onModalWillShow={[Function]}
pointerEvents="box-none"
scrollHorizontal={false}
scrollOffset={0}
scrollOffsetMax={0}
scrollTo={null}
......@@ -504,7 +514,7 @@ exports[`LoginForm component should renders correctly 1`] = `
"padding": 10,
},
Object {
"color": "white",
"color": "#FFFFFF",
},
]
}
......@@ -590,6 +600,11 @@ exports[`LoginForm component should renders correctly 1`] = `
"textColor": undefined,
"value": "zh",
},
Object {
"label": "Slovak",
"textColor": undefined,
"value": "sk",
},
]
}
onChange={[Function]}
......@@ -620,7 +635,9 @@ exports[`LoginForm component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -632,8 +649,10 @@ exports[`LoginForm component should renders correctly 1`] = `
"alignItems": "center",
"backgroundColor": "white",
"borderColor": "#777777",
"borderRadius": 15,
"borderRadius": 20,
"borderWidth": 1,
"flexDirection": "row",
"justifyContent": "center",
"margin": 4,
"opacity": 1,
"padding": 4,
......@@ -657,7 +676,9 @@ exports[`LoginForm component should renders correctly 1`] = `
</View>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -669,8 +690,10 @@ exports[`LoginForm component should renders correctly 1`] = `
"alignItems": "center",
"backgroundColor": "white",
"borderColor": "#4690D6",
"borderRadius": 15,
"borderRadius": 20,
"borderWidth": 1,
"flexDirection": "row",
"justifyContent": "center",
"margin": 4,
"opacity": 1,
"padding": 4,
......
......@@ -42,7 +42,11 @@ exports[`LoginScreen component should renders correctly 1`] = `
>
<Image
resizeMode="contain"
source={1}
source={
Object {
"testUri": "../../../src/assets/logos/logo-white.png",
}
}
style={
Object {
"alignSelf": "center",
......
......@@ -105,7 +105,9 @@ exports[`RegisterForm component should renders correctly 1`] = `
/>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -181,7 +183,9 @@ exports[`RegisterForm component should renders correctly 1`] = `
</View>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -296,7 +300,9 @@ exports[`RegisterForm component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -402,7 +408,9 @@ exports[`RegisterForm component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......
......@@ -7,9 +7,12 @@ import blogFakeFactory from '../../__mocks__/fake/blogs/BlogFactory'
import BlogModel from '../../src/blogs/BlogModel';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
import Actions from '../../src/newsfeed/activity/Actions';
Linking.openURL = jest.fn();
jest.mock('../../src/newsfeed/activity/Actions', () => 'Actions');
/**
* Tests
*/
......@@ -44,7 +47,7 @@ describe('blog card component', () => {
wrapper.instance().navToBlog();
// expect fn to be called once
expect(navigation.push).toBeCalledWith('BlogView', {blog:blogEntity});
expect(navigation.push).toBeCalledWith('BlogView', {blog: blogEntity});
done();
} catch(e) {
done.fail(e);
......
......@@ -90,6 +90,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "1",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -98,6 +99,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -128,6 +130,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -156,6 +159,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -216,6 +220,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "2",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -224,6 +229,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -254,6 +260,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -282,6 +289,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -342,6 +350,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "3",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -350,6 +359,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -380,6 +390,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -408,6 +419,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -534,7 +546,9 @@ exports[`blog list screen component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -571,7 +585,9 @@ exports[`blog list screen component should renders correctly 1`] = `
</View>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -642,6 +658,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "1",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -650,6 +667,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -680,6 +698,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -708,6 +727,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -785,6 +805,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "2",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -793,6 +814,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -823,6 +845,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -851,6 +874,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......@@ -928,6 +952,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"guid": "3",
"header_bg": "1",
"header_top": "0",
"impressions": 100,
"last_save": "1524843907",
"last_updated": "1524838665",
"license": "attribution-noncommercial-cc",
......@@ -936,6 +961,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"ownerObj": Object {
"access_id": "2",
"banned": "no",
"blocked": false,
"boostProPlus": false,
"boost_autorotate": true,
"boost_rating": "2",
......@@ -966,6 +992,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"getAvatarSource": [MockFunction],
"guid": 100,
"icontime": "1523515420",
"impressions": 100,
"language": "en",
"legacy_guid": false,
"mature": "1",
......@@ -994,6 +1021,7 @@ exports[`blog list screen component should renders correctly 1`] = `
"spam": "0",
"subscribed": false,
"subscriber": true,
"subscribers_count": 50,
"subtype": false,
"time_created": "1468113204",
"time_updated": false,
......
import 'react-native';
import React from 'react';
import { Platform, CameraRoll, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import CaptureGallery from '../../src/capture/CaptureGallery';
import { Platform, TouchableOpacity } from "react-native";
import { shallow, render } from 'enzyme';
import CameraRoll from '@react-native-community/cameraroll';
import androidPermission from '../../src/common/services/android-permissions.service';
import { getPhotosFaker } from '../../__mocks__/fake/CameraRollFaker';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
jest.mock('../../src/common/services/android-permissions.service');
jest.mock('@react-native-community/cameraroll');
CameraRoll.getPhotos = jest.fn();
// fake camera roll data
const response = getPhotosFaker(5);
CameraRoll.getPhotos.mockResolvedValue(response);
import CaptureGallery from '../../src/capture/CaptureGallery';
/**
* Tests
*/
describe('cature gallery component', () => {
beforeEach(() => {
CameraRoll.getPhotos.mockClear();
androidPermission.checkReadExternalStorage.mockClear();
androidPermission.readExternalStorage.mockClear();
});
it('should renders correctly', () => {
it('should renders correctly', async() => {
const galley = renderer.create(
<CaptureGallery />
<CaptureGallery />
).toJSON();
expect(galley).toMatchSnapshot();
});
it('should load photos on mount', () => {
const spyWillMount = jest.spyOn(CaptureGallery.prototype, '_loadPhotos');
const spyWillMount = jest.spyOn(CaptureGallery.prototype, 'loadPhotos');
Platform.OS = 'ios';
......@@ -35,7 +43,6 @@ describe('cature gallery component', () => {
<CaptureGallery />
);
// the call is dalayed (setTimeout) so we fast-forward timers
jest.runAllTimers();
......@@ -78,30 +85,20 @@ describe('cature gallery component', () => {
it('should calls onSelected when the user select an image', async(done) => {
// fake camera roll data
const response = getPhotosFaker(5);
CameraRoll.getPhotos = jest.fn();
CameraRoll.getPhotos.mockResolvedValue(response);
const mockFn = jest.fn();
try {
const wrapper = shallow(
<CaptureGallery onSelected={mockFn}/>
);
const wrapper = renderer.create(<CaptureGallery onSelected={mockFn}/>);
// load phoyos
await wrapper.instance()._loadPhotos();
await wrapper.getInstance()._loadPhotos();
// update component
wrapper.update();
expect( CameraRoll.getPhotos).toBeCalled();
// find TouchableOpacity (rendered images in lists)
const images = wrapper.find(TouchableOpacity);
const images = wrapper.root.findAllByType(TouchableOpacity);
// simulate press on image
images.at(1).simulate('press');
images[0].props.onPress();
// expect fn to be called once
expect(mockFn).toBeCalled();
......@@ -112,27 +109,16 @@ describe('cature gallery component', () => {
});
it('should show loaded images', async(done) => {
// fake camera roll data
const response = getPhotosFaker(5);
CameraRoll.getPhotos = jest.fn();
CameraRoll.getPhotos.mockResolvedValue(response);
const mockFn = jest.fn();
try {
const wrapper = shallow(
<CaptureGallery onSelected={mockFn}/>
);
const wrapper = renderer.create(<CaptureGallery onSelected={mockFn}/>);
// load phoyos
await wrapper.instance()._loadPhotos();
// update component
wrapper.update();
await wrapper.getInstance()._loadPhotos();
// find TouchableOpacity (rendered images in lists)
const images = wrapper.find(TouchableOpacity);
const images = wrapper.root.findAllByType(TouchableOpacity);
// expect 5 images rendered
expect(images.length).toEqual(5);
......
......@@ -3,22 +3,29 @@ import React from 'react';
import { Alert } from 'react-native';
import { shallow } from 'enzyme';
import { Icon } from 'react-native-elements'
import CameraRoll from '@react-native-community/cameraroll';
import CapturePoster from '../../src/capture/CapturePoster';
import CapturePreview from '../../src/capture/CapturePreview';
import CaptureGallery from '../../src/capture/CaptureGallery';
import UserStore from '../../src/auth/UserStore';
import CaptureStore from '../../src/capture/CaptureStore';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
import { getPhotosFaker } from '../../__mocks__/fake/CameraRollFaker';
jest.mock('../../src/auth/UserStore');
jest.mock('../../src/capture/CaptureStore');
jest.mock('../../src/capture/CapturePostButton', () => 'CapturePostButton');
jest.mock('../../src/capture/CapturePosterFlags', () => 'CapturePosterFlags');
jest.mock('../../src/capture/CapturePreview', () => 'CapturePreview');
jest.mock('../../src/capture/CapturePosterFlags', () => 'CapturePosterFlags');
jest.mock('../../src/common/services/translation.service');
Alert.alert = jest.fn();
CameraRoll.getPhotos = jest.fn();
// fake camera roll data
const response = getPhotosFaker(5);
CameraRoll.getPhotos.mockResolvedValue(response);
/**
* Tests
......@@ -43,14 +50,15 @@ describe('cature poster component', () => {
});
it('should renders correctly', () => {
const galley = renderer.create(
const screen = renderer.create(
<CapturePoster.wrappedComponent
user={userStore}
capture={capture}
navigation={navigation}
/>
).toJSON();
expect(galley).toMatchSnapshot();
expect(screen).toMatchSnapshot();
});
it('should receive text parameters on did mount', () => {
......@@ -118,8 +126,12 @@ describe('cature poster component', () => {
it('should show the preview when an image is attached', async (done) => {
try {
// emulate image attachment
capture.attachment.hasAttachment = true;
capture.attachment.uri = paramsImage.uri;
capture.attachment.type = paramsImage.type;
const wrapper = shallow(
const wrapper = renderer.create(
<CapturePoster.wrappedComponent
user={userStore}
capture={capture}
......@@ -127,18 +139,14 @@ describe('cature poster component', () => {
/>
);
// emulate image attachment
capture.attachment.hasAttachment = true;
capture.attachment.uri = paramsImage.uri;
capture.attachment.type = paramsImage.type;
const gallery = wrapper.root.findByType(CaptureGallery);
// update component
wrapper.update();
await gallery.instance._loadPhotos();
// find Capture Preview
const preview = wrapper.find(CapturePreview);
const preview = wrapper.root.findByType(CapturePreview);
expect(preview.length).toBe(1);
expect(preview).toBeDefined();
done();
} catch (e) {
......@@ -148,8 +156,12 @@ describe('cature poster component', () => {
it('should show the preview when a video is attached', async (done) => {
try {
// emulate video attachment
capture.attachment.hasAttachment = true;
capture.attachment.uri = paramsVideo.uri;
capture.attachment.type = paramsVideo.type;
const wrapper = shallow(
const wrapper = renderer.create(
<CapturePoster.wrappedComponent
user={userStore}
capture={capture}
......@@ -157,18 +169,14 @@ describe('cature poster component', () => {
/>
);
// emulate video attachment
capture.attachment.hasAttachment = true;
capture.attachment.uri = paramsVideo.uri;
capture.attachment.type = paramsVideo.type;
const gallery = wrapper.root.findByType(CaptureGallery);
// update component
wrapper.update();
await gallery.instance._loadPhotos();
// find Capture Preview
const preview = wrapper.find(CapturePreview);
const preview = wrapper.root.findByType(CapturePreview);
expect(preview.length).toBe(1);
expect(preview).toBeDefined();
done();
} catch (e) {
......@@ -184,7 +192,7 @@ describe('cature poster component', () => {
capture.attachment.uri = paramsVideo.uri;
capture.attachment.type = paramsVideo.type;
const wrapper = shallow(
const wrapper = renderer.create(
<CapturePoster.wrappedComponent
user={userStore}
capture={capture}
......@@ -192,13 +200,17 @@ describe('cature poster component', () => {
/>
);
const gallery = wrapper.root.findByType(CaptureGallery);
await gallery.instance._loadPhotos();
// find delete icon
const icon = wrapper.find(Icon);
const icon = wrapper.root.findByType(Icon);
expect(icon.length).toBe(1);
expect(icon).toBeDefined();
// simulate press on image
icon.at(0).simulate('press');
icon.props.onPress();
// should be called
expect(capture.attachment.delete).toHaveBeenCalled();
......@@ -319,7 +331,8 @@ describe('cature poster component', () => {
// should be called only once
expect(capture.post.mock.calls.length).toBe(1);
expect(capture.post.mock.calls[0][0]).toEqual({ nsfw: [], message: "some awesome post", wire_threshold: null});
const entity = capture.post.mock.calls[0][0];
expect(capture.post.mock.calls[0][0]).toEqual({ nsfw: [], message: "some awesome post", wire_threshold: null, "time_created": entity.time_created});
expect(result).toEqual(response)
......@@ -388,12 +401,14 @@ describe('cature poster component', () => {
// should be called only once
expect(capture.post.mock.calls.length).toBe(1);
const entity = capture.post.mock.calls[0][0];
expect(capture.post.mock.calls[0][0]).toEqual({
nsfw: [],
message: "some awesome post",
wire_threshold: null,
facebook: 1,
twitter: 1
twitter: 1,
time_created: entity.time_created
});
done();
......@@ -434,14 +449,15 @@ describe('cature poster component', () => {
// should be called only once
expect(capture.post).toBeCalled();
const entity = capture.post.mock.calls[0][0];
// should send the attachment data
console.log(capture.post.mock.calls);
expect(capture.post.mock.calls[0][0]).toEqual({
nsfw: [],
message: "some awesome post",
wire_threshold: null,
attachment_guid: 1000,
attachment_license: ''}
attachment_license: '',
time_created: entity.time_created}
);
// should return server response
expect(result).toEqual(response)
......
......@@ -2,7 +2,6 @@ import 'react-native';
import React from 'react';
import { shallow } from 'enzyme';
import { configure } from 'mobx';
import LicensePicker from '../../src/common/components/LicensePicker';
import CaptureStore from '../../src/capture/CaptureStore';
import CapturePosterFlags from '../../src/capture/CapturePosterFlags';
import HashtagStore from '../../src/common/stores/HashtagStore';
......@@ -15,6 +14,18 @@ jest.mock('../../src/common/stores/HashtagStore');
jest.mock('../../src/capture/CaptureStore');
jest.mock('../../src/common/components/LicensePicker', () => 'LicensePicker');
jest.mock('../../src/newsfeed/topbar/TagsSubBar', () => 'TagsSubBar');
jest.mock('../../src/common/components/nsfw/NsfwToggle', () => 'NsfwToggle');
Date = class extends Date {
constructor(date) {
if (date) {
return super(date);
}
return new Date('2018-09-20T23:00:00Z');
}
}
defaultState = {
......@@ -53,6 +64,7 @@ const testRenderWithValue = (value) => {
onMature={fn}
onShare={fn}
onLocking={fn}
onScheduled={fn}
/>
).toJSON();
expect(preview).toMatchSnapshot();
......@@ -104,7 +116,7 @@ describe('cature poster flags component', () => {
const hashtagStore = new HashtagStore();
store.loadSuggestedTags.mockResolvedValue();
const capturePosterFlag = renderer.create(
let capturePosterFlag = renderer.create(
<CapturePosterFlags
capture={store}
hashtag={hashtagStore}
......@@ -125,6 +137,19 @@ describe('cature poster flags component', () => {
store.attachment.hasAttachment = true;
capturePosterFlag = renderer.create(
<CapturePosterFlags
capture={store}
hashtag={hashtagStore}
matureValue={defaultState.mature}
shareValue={defaultState.share}
lockValue={defaultState.lock}
onMature={fn}
onShare={fn}
onLocking={fn}
/>
);
picker = capturePosterFlag.root.findAllByType('LicensePicker');
// check there is 1 license picker
......
......@@ -27,7 +27,9 @@ exports[`cature tabs component should renders correctly 1`] = `
>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -94,7 +96,9 @@ exports[`cature tabs component should renders correctly 1`] = `
</View>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......@@ -161,7 +165,9 @@ exports[`cature tabs component should renders correctly 1`] = `
</View>
<View
accessible={true}
focusable={true}
isTVSelectable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
......
......@@ -10,18 +10,21 @@ import features from '../../src/common/services/features.service';
import renderer from 'react-test-renderer';
jest.mock('../../src/channel/ChannelStore');
jest.mock('../../src/channel/UserModel');
jest.mock('../../src/common/services/features.service');
jest.mock('../../AppStores');
jest.mock('../../src/common/services/boosted-content.service');
/**
* Tests
*/
describe('channel actions component', () => {
let store;
beforeEach(() => {
store = new ChannelStore();
store.channel = new UserModel(userFaker(1));
store.channel.toggleSubscription = jest.fn();
store.channel.toggleBlock = jest.fn();
});
it('should renders correctly', () => {
......@@ -68,13 +71,13 @@ describe('channel actions component', () => {
store.channel.subscribed = true;
store.channel.blocked = true;
opt = wrapper.instance().makeAction(1);
opt = wrapper.instance().executeAction(1);
expect(store.channel.toggleSubscription).toBeCalled();
opt = wrapper.instance().makeAction(2);
expect(store.toggleBlock).toBeCalled();
opt = wrapper.instance().executeAction(2);
expect(store.channel.toggleBlock).toBeCalled();
opt = wrapper.instance().makeAction(3);
opt = wrapper.instance().executeAction(3);
expect(navigation.push).toBeCalled();
});
......
import 'react-native';
import React from 'react';
//import { Platform, CameraRoll, TouchableOpacity } from "react-native";
import { shallow } from 'enzyme';
import ChannelScreen from '../../src/channel/ChannelScreen';
import { activitiesServiceFaker } from '../../__mocks__/fake/ActivitiesFaker';
import renderer from 'react-test-renderer';
import UserStore from '../../src/auth/UserStore';
import RewardsCarousel from '../../src/channel/carousel/RewardsCarousel';
import ChannelHeader from '../../src/channel/header/ChannelHeader';
import Toolbar from '../../src/channel/toolbar/Toolbar';
import NewsfeedList from '../../src/newsfeed/NewsfeedList';
import channelService from '../../src/channel/ChannelService';
import CenteredLoading from '../../src/common/components/CenteredLoading';
import Button from '../../src/common/components/Button';
import colors from '../../src/styles/Colors';
import BlogCard from '../../src/blogs/BlogCard';
import CaptureFab from '../../src/capture/CaptureFab';
import { CommonStyle } from '../../src/styles/Common';
import UserModel from '../../src/channel/UserModel';
import Touchable from '../../src/common/components/Touchable';
import session from '../../src/common/services/session.service';
import ChannelStore from '../../src/channel/ChannelStore';
import userFaker from '../../__mocks__/fake/channel/UserFactory'
import UserModel, { USER_MODE_CLOSED, USER_MODE_OPEN } from '../../src/channel/UserModel';
import featuresService from '../../src/common/services/features.service';
jest.mock('../../src/common/helpers/abortableFetch');
jest.mock('../../src/channel/UserModel');
jest.mock('../../src/newsfeed/ActivityModel');
jest.mock('../../src/channel/carousel/RewardsCarousel', () => 'RewardsCarousel');
jest.mock('../../src/channel/header/ChannelHeader', () => 'ChannelHeader');
......@@ -37,13 +18,14 @@ jest.mock('../../src/common/components/FeedList', () => 'FeedList');
jest.mock('../../src/capture/CaptureFab', () => 'CaptureFab');
jest.mock('../../src/blogs/BlogCard', () => 'BlogCard');
jest.mock('../../src/common/components/Touchable', () => 'Touchable');
jest.mock('../../src/common/services/boosted-content.service');
/**
* Tests
*/
describe('Channel screen component', () => {
let store, screen, entities, channel, navigation, activityResponse, user;
let screen, channel, navigation, user, channelStore;
beforeEach(() => {
......@@ -52,17 +34,14 @@ describe('Channel screen component', () => {
activity.wire_totals = { tokens: 20};
activity.impressions = 20;
navigation = { navigate: jest.fn() };
navigation = { navigate: jest.fn(), goBack: jest.fn() };
navigation.state = {
params: {
entity: activity
}
};
channelStore = {
channel: {
guid:'125',
blocked: false,
},
channel: new UserModel(userFaker(1)),
setChannel: jest.fn(),
feedStore: {
setChannel: jest.fn(),
......@@ -73,16 +52,14 @@ describe('Channel screen component', () => {
load: jest.fn(),
};
user = new UserStore();
channel = { store: jest.fn() };
channel.store.mockReturnValue(channelStore);
screen = shallow(
<ChannelScreen.wrappedComponent channel={channel} user={user} navigation={navigation} />
<ChannelScreen.wrappedComponent channel={channel} navigation={navigation} />
);
screen.update();
});
it('should renders correctly', () => {
......@@ -90,8 +67,7 @@ describe('Channel screen component', () => {
expect(screen).toMatchSnapshot();
});
it('should have the expectedComponents while loading, also check newsfeed', async () => {
screen.update();
it('should have the expected components while loading, also check newsfeed', async () => {
expect(screen.find('FeedList')).toHaveLength(1);
let render = screen.dive();
......@@ -99,36 +75,42 @@ describe('Channel screen component', () => {
expect(screen.find('CaptureFab')).toHaveLength(1);
});
it('should have the expected components while loading, also check flatlist for blocked', async () => {
it('should have the expectedComponents while loading, also check flatlist for blocked', async () => {
screen.update();
channelStore = {
channel: {
guid:'125',
blocked: true,
},
setChannel: jest.fn(),
feedStore: {
setChannel: jest.fn(),
},
rewards: {
merged: []
},
load: jest.fn(),
};
user = new UserStore();
channel = { store: jest.fn() };
channelStore.channel.blocked = true;
channel.store.mockReturnValue(channelStore);
await screen.update();
screen = shallow(
<ChannelScreen.wrappedComponent channel={channel} user={user} navigation={navigation} />
);
expect(screen.find('FlatList')).toHaveLength(1);
expect(screen.find('FeedList')).toHaveLength(0);
let render = screen.dive();
expect(render.find('CenteredLoading')).toHaveLength(0);
expect(screen.find('CaptureFab')).toHaveLength(1);
});
it('should show closed channel message', async () => {
channelStore.channel.blocked = false;
channelStore.channel.mode = USER_MODE_CLOSED;
await screen.update();
expect(screen).toMatchSnapshot();
});
it('should navigate back if the the VIEW permissions are removed', async () => {
channelStore.channel.mode = USER_MODE_OPEN;
// force permissions feature flag
featuresService.features = {permissions: true};
channelStore.channel.setPermissions({permissions:[]});
await screen.update();
expect(navigation.goBack).toBeCalled();
});
});
\ No newline at end of file
......@@ -14,8 +14,7 @@ import appStores from '../../../AppStores';
jest.mock('../../../src/auth/UserStore');
jest.mock('../../../AppStores');
jest.mock('../../../src/channel/ChannelStore');
jest.mock('TouchableHighlight', () => 'TouchableHighlight');
jest.mock('../../../src/common/services/boosted-content.service');
/**
* Tests
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.