Compare commits

...

10 Commits

  1. 83
      .detoxrc.js
  2. 5
      android/app/build.gradle
  3. 29
      android/app/src/androidTest/java/com/taskme2/DetoxTest.java
  4. 3
      android/app/src/main/AndroidManifest.xml
  5. 7
      android/app/src/main/res/xml/network_security_config.xml
  6. 20
      android/build.gradle
  7. 12
      e2e/appStarter.test.js
  8. 122
      e2e/changeWorkPhoneNumber.test.js
  9. 28
      e2e/config.js
  10. 93
      e2e/createReason.test.js
  11. 85
      e2e/createTask.test.js
  12. 273
      e2e/editProfile.test.js
  13. 12
      e2e/jest.config.js
  14. 41
      e2e/logout.test.js
  15. 77
      e2e/signIn.test.js
  16. 22
      e2e/snippets.js
  17. 26
      metro.config.js
  18. 63384
      package-lock.json
  19. 261
      package.json
  20. BIN
      src/assets/images/default_avatar.png
  21. 17
      src/config/index.ts
  22. 1
      src/modules/account/atoms/edit-avatar.atom.tsx
  23. 10
      src/modules/account/components/account-phone-number-input.component.tsx
  24. 4
      src/modules/account/components/change-date-of-birthday-moda.component.tsx
  25. 2
      src/modules/account/components/change-phone-number-modal.component.tsx
  26. 3
      src/modules/account/components/fake-date-input-with-modal.component.tsx
  27. 1
      src/modules/account/components/work-phone-number.component.tsx
  28. 4
      src/modules/account/hooks/use-account-editor.hook.ts
  29. 12
      src/modules/account/screens/account.screen.tsx
  30. 7
      src/modules/auth/screens/confirm-code.screen.tsx
  31. 4
      src/modules/auth/screens/sign-in.screen.tsx
  32. 1
      src/modules/home/screens/home.screen.tsx
  33. 5
      src/modules/root/atoms/icon-with-count-indicator.component.tsx
  34. 4
      src/modules/root/components/tab-bar.component.tsx
  35. 2
      src/modules/root/smart-components/confirm-modal.smart-component.tsx
  36. 3
      src/modules/root/smart-components/date-picker.smart-component.tsx
  37. 5
      src/modules/root/smart-components/info-modal.smart-component.tsx
  38. 14
      src/modules/tasks/atoms/add-task-row.atom.tsx
  39. 1
      src/modules/tasks/atoms/create-task-description.atom.tsx
  40. 1
      src/modules/tasks/atoms/create-task-title-field.atom.tsx
  41. 8
      src/modules/tasks/components/add-new-reason-modal.component.tsx
  42. 8
      src/modules/tasks/components/reason-form.component.tsx
  43. 8
      src/modules/tasks/components/set-task-interval.component.tsx
  44. 3
      src/modules/tasks/screens/add-update-task.screen.tsx
  45. 1
      src/modules/tasks/smart-components/add-new-reason.smart-component.tsx
  46. 1
      src/modules/tasks/smart-components/select-task-executor-list.smart-component.tsx
  47. 29
      src/providers/ImageCropPickerOpenPickerProvider.e2e.js
  48. 2
      src/providers/ImageCropPickerOpenPickerProvider.js
  49. 7
      src/repositories/api/account.repository.ts
  50. 2
      src/repositories/api/chat.repository.ts
  51. 4
      src/services/system/fs.service.ts
  52. 10
      src/services/system/media.service.ts
  53. 31
      src/services/system/reactron.service.ts
  54. 2
      src/services/system/real-time.service.ts
  55. 6
      src/shared/components/buttons/button.component.tsx
  56. 78
      src/shared/components/elements/icon.component.tsx
  57. 3
      src/shared/components/elements/txt.component.tsx
  58. 3
      src/shared/components/forms/form-fake-date-input.component.tsx
  59. 2
      src/shared/components/forms/form-large-control-with-icon.component.tsx
  60. 8
      src/shared/components/forms/form-phone.component.tsx
  61. 4
      src/shared/components/forms/form-select.component.tsx
  62. 11
      src/shared/components/forms/form-text-input-with-icon.component.tsx
  63. 7
      src/shared/components/forms/form-textarea.component.tsx
  64. 14
      src/shared/components/forms/touchable-fake-input.atom.tsx
  65. 4
      src/shared/components/images/img-with-bg-circle.components.tsx
  66. 3
      src/shared/components/layouts/screen-layout-scroll-content.componen.tsx
  67. 13
      src/shared/components/layouts/screen-layout.component.tsx
  68. 14
      src/shared/components/modals/confirm-code-modal.component.tsx
  69. 3
      src/shared/components/plugins/image-crop-picker.component.tsx
  70. 2
      src/shared/components/plugins/masked-input.component.tsx
  71. 2
      src/shared/consts/base64Avatar.js
  72. 2
      src/shared/consts/base64Background.js
  73. 28
      src/shared/helpers/random-string.helper.js

83
.detoxrc.js

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
$0: 'jest',
config: 'e2e/jest.config.js',
},
jest: {
setupTimeout: 120000,
},
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath:
'ios/build/Build/Products/Debug-iphonesimulator/taskme2.app',
build: 'xcodebuild -workspace ios/taskme2.xcworkspace -scheme taskme2 -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
},
'ios.release': {
type: 'ios.app',
binaryPath:
'ios/build/Build/Products/Release-iphonesimulator/taskme2.app',
build: 'xcodebuild -workspace ios/taskme2.xcworkspace -scheme taskme2 -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
reversePorts: [8081],
},
'android.release': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14',
},
},
attached: {
type: 'android.attached',
device: {
adbName: '.*',
},
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_2_API_28',
},
},
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug',
},
'ios.sim.release': {
device: 'simulator',
app: 'ios.release',
},
'android.att.debug': {
device: 'attached',
app: 'android.debug',
},
'android.att.release': {
device: 'attached',
app: 'android.release',
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug',
},
'android.emu.release': {
device: 'emulator',
app: 'android.release',
},
},
}

5
android/app/build.gradle

@ -99,6 +99,8 @@ android { @@ -99,6 +99,8 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 21
versionName "2.1"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
splits {
@ -133,6 +135,7 @@ android { @@ -133,6 +135,7 @@ android {
signingConfig signingConfigs.release
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
}
}
@ -154,6 +157,8 @@ android { @@ -154,6 +157,8 @@ android {
}
dependencies {
androidTestImplementation('com.wix:detox:+')
implementation 'androidx.appcompat:appcompat:1.1.0'
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")

29
android/app/src/androidTest/java/com/taskme2/DetoxTest.java

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
package com.app.task_me;
import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class DetoxTest {
@Rule // (2)
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
@Test
public void runDetoxTests() {
DetoxConfig detoxConfig = new DetoxConfig();
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);
Detox.runTests(mActivityRule, detoxConfig);
}
}

3
android/app/src/main/AndroidManifest.xml

@ -33,7 +33,8 @@ @@ -33,7 +33,8 @@
android:allowBackup="false"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config">
<meta-data
android:name="com.onesignal.NotificationServiceExtension"

7
android/app/src/main/res/xml/network_security_config.xml

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>

20
android/build.gradle

@ -9,7 +9,7 @@ buildscript { @@ -9,7 +9,7 @@ buildscript {
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
kotlin_version = '1.6.21'
kotlin_version = '1.8.0'
}
repositories {
google()
@ -23,4 +23,22 @@ buildscript { @@ -23,4 +23,22 @@ buildscript {
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
allprojects {
repositories {
google()
maven {
url("$rootDir/../node_modules/detox/Detox-android")
}
maven { url 'https://www.jitpack.io' }
}
afterEvaluate {
if (it.hasProperty('android')){
android {
defaultConfig {
minSdkVersion 25
}
}
}
}
}
}

12
e2e/appStarter.test.js

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
describe('Start', () => {
beforeAll(async () => {
await device.launchApp({
newInstance: true,
delete: true,
})
})
it('Should have sign in screen when app is started', async () => {
await expect(element(by.id('signInScreen'))).toBeVisible()
})
})

122
e2e/changeWorkPhoneNumber.test.js

@ -0,0 +1,122 @@ @@ -0,0 +1,122 @@
/* eslint-disable no-undef */
const { config } = require('@/config')
const { insertPhoneAndEnter, goToProfileScreen } = require('./snippets')
const { newPhoneNumber } = require('./config')
describe('Change work phone number', () => {
beforeAll(async () => {
await device.launchApp({
permissions: {
notifications: 'YES',
},
newInstance: true,
delete: true,
})
})
it('Should success sign in the app with default phone number (need no confirmation)', async () => {
await insertPhoneAndEnter(config.defaultPhoneNumber)
await expect(element(by.id('homeScreen'))).toExist()
})
it('Should navigate to profile screen', async () => {
await goToProfileScreen()
await expect(element(by.text('Налаштування профілю'))).toBeVisible()
})
it('Should open work phone number input modal', async () => {
await waitFor(element(by.id(`workPhone`)))
.toBeVisible()
.whileElement(by.id('profileScreenScroll'))
.scroll(250, 'down')
await element(by.id(`workPhone`)).tap()
await expect(element(by.id('workPhoneInput'))).toBeVisible()
})
it('Should not go further and show error message when trying to go ahead with empty phone number field', async () => {
await element(by.id('changeWorkPhoneBtn')).tap()
await expect(element(by.id('workPhoneInputError'))).toBeVisible()
})
it('Should not go further and show error message when trying to go ahead with phone number existing in DB', async () => {
await element(by.id('workPhoneInput')).typeText(
config.defaultPhoneNumber,
)
await element(by.id('changeWorkPhoneBtn')).tap()
await expect(element(by.id('workPhoneInputError'))).toBeVisible()
await expect(element(by.id('workPhoneInputError'))).toHaveText(
'Номер зайнятий',
)
})
it('Should show OTP input field when trying to go ahead with valid phone number that is not in DB', async () => {
await element(by.id('workPhoneInput')).clearText()
await element(by.id('workPhoneInput')).typeText(newPhoneNumber)
await element(by.id('changeWorkPhoneBtn')).tap()
await expect(element(by.id('confirmCodeInput'))).toBeVisible()
})
it('Should not go further and show error message when trying to confirm change work phone number with empty OTP', async () => {
await element(by.id('confirmCodeBtn')).tap()
await expect(
element(
by.id('confirmCodeInputError').and(by.text("Обов'язкове поле")),
),
).toBeVisible()
})
it('Should not go further and show error message when trying to confirm change work phone number with invalid OTP', async () => {
await element(by.id('confirmCodeInput')).typeText('0000')
await element(by.id('confirmCodeBtn')).tap()
await expect(element(by.text('Не дійсний код'))).toBeVisible()
})
it('Should "Надіслати код повторно" interactive text be displayed after timer expires, press that text restarts the timer', async () => {
await waitFor(element(by.text('Надіслати код повторно')))
.toBeVisible()
.withTimeout(config.initialTimerCount * 1000)
await element(by.text('Надіслати код повторно')).tap()
await expect(element(by.id('timer'))).toBeVisible()
})
it('Should success change work phone number with correct OTP', async () => {
await element(by.id('confirmCodeInput')).clearText()
await element(by.id('confirmCodeInput')).typeText('1234')
await expect(element(by.id('signInScreen'))).toBeVisible()
})
it('Should success sign in with new work phone number', async () => {
await insertPhoneAndEnter(newPhoneNumber)
await element(by.id('signInCodeInput')).typeText('1234')
await expect(element(by.id('homeScreen'))).toExist()
})
it('Should change back work phone number to initial', async () => {
await goToProfileScreen()
await waitFor(element(by.id(`workPhone`)))
.toBeVisible()
.whileElement(by.id('profileScreenScroll'))
.scroll(250, 'down')
await element(by.id(`workPhone`)).tap()
await element(by.id('workPhoneInput')).typeText(
config.defaultPhoneNumber,
)
await element(by.id('changeWorkPhoneBtn')).tap()
await element(by.id('confirmCodeInput')).typeText('1234')
await expect(element(by.id('signInScreen'))).toBeVisible()
})
})

28
e2e/config.js

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
import { makeRandomString } from '@/shared/helpers/random-string.helper'
export const fieldErrors = {
profileDOBInput: 'Вам має бути не менше 18 років',
profileNameInput: 'Мінімальна довжина становить 2 символи',
profileLastNameInput: 'Мінімальна довжина становить 2 символи',
profileMiddleNameInput: 'Мінімальна довжина становить 2 символи',
profilePositionInput: 'Мінімальна довжина становить 2 символи',
profilePersonalPhoneInput: 'Номер не вірний',
profileInnerPhoneInput: 'Мінімальна довжина становить 4 цифри',
profileEmailInput: 'Введений email не вірний',
}
export const validProfileData = {
DOB: '2000-02-06T05:10:00-08:00',
Name: 'First',
LastName: 'Last',
MiddleName: 'Middle',
Position: 'Position',
PersonalPhone: `38099${makeRandomString(7, 'num')}`,
InnerPhone: '12345',
Email: `${makeRandomString()}@mail.com`,
}
export const newPhoneNumber = makeRandomString(12, 'num')
export const personalReason = 'Персональна ' + makeRandomString(5)
export const commonReason = 'Загальна ' + makeRandomString(5)

93
e2e/createReason.test.js

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
/* eslint-disable no-undef */
const { config } = require('@/config')
const { insertPhoneAndEnter, goToCreateTaskScreen } = require('./snippets')
const { personalReason, commonReason } = require('./config')
const openAddReasonModal = async () => {
await waitFor(element(by.id('addReasonInput')))
.toBeVisible()
.whileElement(by.id('editTaskScreenScroll'))
.scroll(50, 'down')
await element(by.id('addReasonInput')).tap()
}
describe('Create task reason', () => {
beforeAll(async () => {
await device.launchApp({
permissions: {
notifications: 'YES',
},
newInstance: true,
delete: true,
})
})
it('Should success sign in the app with default phone number (need no confirmation)', async () => {
await insertPhoneAndEnter(config.defaultPhoneNumber)
await expect(element(by.id('homeScreen'))).toExist()
})
it('Should navigate to create task screen', async () => {
await goToCreateTaskScreen()
await expect(element(by.text('Нова задача'))).toBeVisible()
})
it('Should open add new reason modal', async () => {
await openAddReasonModal()
await expect(element(by.id('reasonInput'))).toBeVisible()
})
it('Should close add reason modal and show error when trying to save personal reason with empty reason name field', async () => {
await element(by.id('savePersonalReasonBtn')).tap()
await expect(element(by.id('reasonInput'))).not.toBeVisible()
await expect(
element(by.id('infoModalTxt').and(by.text('Виникла помилка'))),
).toBeVisible()
await element(by.id('infoModalBtn')).tap()
})
it('Should close add reason modal and show error when trying to save common reason with empty reason name field', async () => {
await openAddReasonModal()
await element(by.id('saveCommonReasonBtn')).tap()
await expect(element(by.id('reasonInput'))).not.toBeVisible()
await expect(
element(by.id('infoModalTxt').and(by.text('Виникла помилка'))),
).toBeVisible()
await element(by.id('infoModalBtn')).tap()
})
it('Should successful add new personal reason and automatically set new reason as task reason', async () => {
await openAddReasonModal()
if (device.getPlatform() === 'android')
await element(by.id('reasonInput')).replaceText(personalReason)
else await element(by.id('reasonInput')).typeText(personalReason)
await element(by.id('savePersonalReasonBtn')).tap()
await expect(element(by.id('reasonInput'))).not.toBeVisible()
await expect(element(by.id('reasonSelectValue'))).toHaveText(
personalReason,
)
})
it('Should successful add new common reason and automatically set new reason as task reason', async () => {
await openAddReasonModal()
if (device.getPlatform() === 'android')
await element(by.id('reasonInput')).replaceText(commonReason)
else await element(by.id('reasonInput')).typeText(commonReason)
await element(by.id('saveCommonReasonBtn')).tap()
await expect(element(by.id('reasonInput'))).not.toBeVisible()
await expect(element(by.id('reasonSelectValue'))).toHaveText(
commonReason,
)
})
})

85
e2e/createTask.test.js

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
const { config } = require('@/config')
const { insertPhoneAndEnter, goToCreateTaskScreen } = require('./snippets')
const requiredFields = ['executor', 'initiator', 'description', 'title']
const currentDate = new Date()
const checkFieldHasError = async fieldName => {
const field = `task${fieldName.charAt(0).toUpperCase()}${fieldName.slice(
1,
)}`
await waitFor(
element(
by
.id(`${field}Error`)
.and(by.text("Поле обов'язкове до заповнення")),
),
)
.toBeVisible()
.whileElement(by.id('editTaskScreenScroll'))
.scroll(250, 'up')
}
describe('Create task reason', () => {
beforeAll(async () => {
await device.launchApp({
permissions: {
notifications: 'YES',
},
newInstance: true,
delete: true,
})
})
it('Should success sign in the app with default phone number (need no confirmation)', async () => {
await insertPhoneAndEnter(config.defaultPhoneNumber)
await expect(element(by.id('homeScreen'))).toExist()
})
it('Should navigate to create task screen', async () => {
await goToCreateTaskScreen()
await expect(element(by.text('Нова задача'))).toBeVisible()
})
// ***** TEST SAVE EMPTY FIELDS *****
it('Should show errors under required fields when trying to create task without filling in those fields', async () => {
await element(by.id('editTaskScreenScroll')).scrollTo('bottom')
await element(by.id('saveTaskBtn')).tap()
for await (const field of requiredFields) {
await checkFieldHasError(field)
}
})
it("Should show error when trying to create task with end date that doesn't meet requirements", async () => {
await element(by.id('editTaskScreenScroll')).scrollTo('top')
await element(by.id('taskStartDate')).tap()
if (device.getPlatform() === 'android') {
const date = currentDate.getDate()
await element(by.text(date.toString())).swipe('up', 'slow')
await element(by.id('datePickerView')).tap({ x: 30, y: -200 })
} else {
const datePicker = element(by.id('datePicker'))
await datePicker.setDatePickerDate(
new Date(
currentDate.setDate(currentDate.getDate() + 2),
).toISOString(),
'ISO8601',
)
await element(by.type('RCTRootContentView')).tap({ x: 0, y: 0 })
}
await element(by.id('editTaskScreenScroll')).scrollTo('bottom')
await element(by.id('saveTaskBtn')).tap()
await waitFor(element(by.id('taskDateError')))
.toBeVisible()
.whileElement(by.id('editTaskScreenScroll'))
.scroll(250, 'up')
await expect(element(by.id('taskDateError'))).toBeVisible()
await expect(element(by.id('taskDateError'))).toHaveText(
'Кінцева дата не може бути меншою, ніж початкова',
)
})
})

273
e2e/editProfile.test.js

@ -0,0 +1,273 @@ @@ -0,0 +1,273 @@
/* eslint-disable no-undef */
const { config } = require('@/config')
const { insertPhoneAndEnter, goToProfileScreen } = require('./snippets')
const { fieldErrors, validProfileData } = require('./config')
const testSaveClearedField = async fieldName => {
await element(by.id(`profile${fieldName}Input`)).clearText()
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileSaveBtn')).tap()
await waitFor(
element(
by
.id(`profile${fieldName}InputError`)
.and(by.text('Поле обовязкове до заповнення')),
),
)
.toBeVisible()
.whileElement(by.id('profileScreenScroll'))
.scroll(250, 'up')
}
const testSaveInvalidData = async (fieldName, newValue) => {
const field = `profile${fieldName}Input`
await element(by.id(field)).replaceText(newValue)
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileSaveBtn')).tap()
await waitFor(
element(
by
.id(`profile${fieldName}InputError`)
.and(by.text(fieldErrors[field])),
),
)
.toBeVisible()
.whileElement(by.id('profileScreenScroll'))
.scroll(250, 'up')
}
const doBeforeActions = async () => {
if (device.getPlatform() === 'android')
await device.launchApp({ newInstance: true })
else await device.reloadReactNative()
await goToProfileScreen()
}
const insertNewValue = async (fieldName, newValue) => {
const field = `profile${fieldName}Input`
if (fieldName === 'DOB') {
await element(by.id('profileDOBInput')).tap()
const datePicker = element(by.id('datePicker'))
await datePicker.setDatePickerDate(newValue, 'ISO8601')
await element(by.id('selectDateBtn')).tap()
} else {
await element(by.id(field)).clearText()
await element(by.id(field)).typeText(newValue)
}
}
const fillFields = async (fieldName, newValue) => {
if (device.getPlatform() === 'android' && fieldName === 'DOB') return
const field = `profile${fieldName}Input`
try {
await insertNewValue(fieldName, newValue)
} catch (e) {
await waitFor(element(by.id(field)))
.toBeVisible()
.whileElement(by.id('profileScreenScroll'))
.scroll(250, 'down')
await insertNewValue(fieldName, newValue)
}
}
describe('Edit profile', () => {
beforeAll(async () => {
await device.launchApp({
permissions: {
camera: 'YES',
notifications: 'YES',
medialibrary: 'YES',
photos: 'YES',
},
newInstance: true,
delete: true,
})
})
it('Should success sign in the app with default phone number (need no confirmation)', async () => {
await insertPhoneAndEnter(config.defaultPhoneNumber)
await expect(element(by.id('homeScreen'))).toExist()
})
it('Should navigate to profile screen', async () => {
await goToProfileScreen()
await expect(element(by.text('Налаштування профілю'))).toBeVisible()
})
// ***** TEST SAVE EMPTY FIELDS *****
it('Should show error when save account data with empty name', async () => {
await testSaveClearedField('Name')
})
it('Should show error when save account data with empty last name', async () => {
await testSaveClearedField('LastName')
})
it('Should show error when save account data with empty middle name', async () => {
await testSaveClearedField('MiddleName')
})
it('Should show error when save account data with empty position', async () => {
await testSaveClearedField('Position')
})
// ***** TEST SAVE INVALID FIELDS *****
it('Should show error when save account data with invalid date of birth (only IOS)', async () => {
await doBeforeActions()
if (device.getPlatform() !== 'android') {
await element(by.id('profileDOBInput')).tap()
const datePicker = element(by.id('datePicker'))
await datePicker.setDatePickerDate(
new Date().toISOString(),
'ISO8601',
)
await element(by.id('selectDateBtn')).tap()
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileSaveBtn')).tap()
await waitFor(
element(
by
.id('profileDOBInputError')
.and(by.text(fieldErrors.profileDOBInput)),
),
)
.toBeVisible()
.whileElement(by.id('profileScreenScroll'))
.scroll(250, 'up')
} else {
await element(by.id('profileDOBInput')).tap()
await element(by.id('selectDateBtn')).tap()
await waitFor(element(by.id('profileDOBInput')))
.toBeVisible()
.whileElement(by.id('profileScreenScroll'))
.scroll(250, 'up')
}
})
it('Should show error when save account data with invalid first name', async () => {
await testSaveInvalidData('Name', 'A')
})
it('Should show error when save account data with invalid last name', async () => {
await testSaveInvalidData('LastName', 'A')
})
it('Should show error when save account data with invalid middle name', async () => {
await testSaveInvalidData('MiddleName', 'A')
})
it('Should show error when save account data with invalid position', async () => {
await testSaveInvalidData('Position', 'A')
})
it('Should show error when save account data with invalid personal phone number', async () => {
await testSaveInvalidData('PersonalPhone', '1')
})
it('Should show error when save account data with invalid inner phone number', async () => {
await testSaveInvalidData('InnerPhone', '1')
})
it('Should show error when save account data with invalid email', async () => {
await testSaveInvalidData('Email', 'invalid email')
})
it('Should update profile and show success message when all fields filled properly', async () => {
await doBeforeActions()
for await (const field of Object.keys(validProfileData)) {
await fillFields(field, validProfileData[field])
}
await element(by.id('profileSaveBtn')).tap()
await element(by.id('confirmModalOkBtn')).tap()
await expect(
element(by.id('infoModalTxt').and(by.text('Дані оновлені.'))),
).toBeVisible()
await element(by.id('infoModalBtn')).tap()
})
it('Should change back email to initial', async () => {
await doBeforeActions()
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileEmailInput')).clearText()
await element(by.id('profileEmailInput')).typeText(
`${config.defaultPhoneNumber}@admin.com`,
)
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileSaveBtn')).tap()
await element(by.id('confirmModalOkBtn')).tap()
await expect(
element(by.id('infoModalTxt').and(by.text('Дані оновлені.'))),
).toBeVisible()
await element(by.id('infoModalBtn')).tap()
})
it('Should show message that email exists when save account data with email already existing in DB', async () => {
await doBeforeActions()
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileEmailInput')).clearText()
await element(by.id('profileEmailInput')).typeText(config.existEmail)
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileSaveBtn')).tap()
await element(by.id('confirmModalOkBtn')).tap()
await expect(
element(
by
.id('infoModalTxt')
.and(by.text('Вказана електронна пошта вже зайнята')),
),
).toBeVisible()
await element(by.id('infoModalBtn')).tap()
})
it('Should update profile avatar and show success message', async () => {
await doBeforeActions()
await element(by.id('profileAvatar')).tap()
await element(by.text('Фото з галереї')).tap()
await expect(element(by.id('clearAvatarBtn'))).toBeVisible()
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileSaveBtn')).tap()
await element(by.id('confirmModalOkBtn')).tap()
await expect(
element(by.id('infoModalTxt').and(by.text('Дані оновлені.'))),
).toBeVisible()
await element(by.id('infoModalBtn')).tap()
})
it('Should clear profile avatar and show success message', async () => {
await doBeforeActions()
await element(by.id('clearAvatarBtn')).tap()
await expect(element(by.id('clearAvatarBtn'))).not.toBeVisible()
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileSaveBtn')).tap()
await element(by.id('confirmModalOkBtn')).tap()
await expect(
element(by.id('infoModalTxt').and(by.text('Дані оновлені.'))),
).toBeVisible()
await element(by.id('infoModalBtn')).tap()
})
})

12
e2e/jest.config.js

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
rootDir: '..',
testMatch: ['<rootDir>/e2e/**/*.test.js'],
testTimeout: 120000,
maxWorkers: 1,
globalSetup: 'detox/runners/jest/globalSetup',
globalTeardown: 'detox/runners/jest/globalTeardown',
reporters: ['detox/runners/jest/reporter'],
testEnvironment: 'detox/runners/jest/testEnvironment',
verbose: true,
};

41
e2e/logout.test.js

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/* eslint-disable no-undef */
const { config } = require('@/config')
const { insertPhoneAndEnter, goToProfileScreen } = require('./snippets')
describe('Logout', () => {
beforeAll(async () => {
await device.launchApp({
permissions: { camera: 'YES', notifications: 'YES' },
newInstance: true,
delete: true,
})
})
it('Should success sign in the app with default phone number (need no confirmation)', async () => {
await insertPhoneAndEnter(config.defaultPhoneNumber)
await expect(element(by.id('homeScreen'))).toExist()
})
it('Should navigate to profile screen', async () => {
await goToProfileScreen()
await expect(element(by.text('Налаштування профілю'))).toBeVisible()
})
it('Should stay on profile screen', async () => {
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileLogoutBtn')).tap()
await element(by.id('confirmModalCancelBtn')).tap()
await expect(element(by.text('Налаштування профілю'))).toBeVisible()
})
it('Should success logout', async () => {
await element(by.id('profileScreenScroll')).scrollTo('bottom')
await element(by.id('profileLogoutBtn')).tap()
await element(by.id('confirmModalOkBtn')).tap()
await expect(element(by.id('signInScreen'))).toBeVisible()
})
})

77
e2e/signIn.test.js

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
/* eslint-disable no-undef */
const { config } = require('@/config')
const { insertPhoneAndEnter } = require('./snippets')
describe('Sign in', () => {
beforeAll(async () => {
await device.launchApp({
permissions: { camera: 'YES', notifications: 'YES' },
newInstance: true,
delete: true,
})
})
beforeEach(async () => {
if (device.getPlatform() === 'android')
await device.launchApp({ newInstance: true, delete: true })
else await device.reloadReactNative()
})
it('Should not go further and show error message when trying to sign with empty phone number', async () => {
await element(by.id('signInBtn')).tap()
await expect(element(by.text("Обов'язкове поле"))).toExist()
})
it('Should not go further and show error message when trying to sign in with empty OTP', async () => {
await insertPhoneAndEnter(config.phoneNumber)
await element(by.id('sendCodeBtn')).tap()
await expect(element(by.text("Обов'язкове поле"))).toBeVisible()
})
it('Should not go further and show error message when trying to sign in with invalid OTP', async () => {
await insertPhoneAndEnter(config.phoneNumber)
await element(by.id('signInCodeInput')).typeText('0000')
await element(by.text('Вхід')).tap()
await element(by.id('sendCodeBtn')).tap()
await expect(element(by.text('Не дійсний код'))).toBeVisible()
})
it('Should "Надіслати код повторно" interactive text be displayed after timer expires, press that text restarts the timer', async () => {
await insertPhoneAndEnter(config.phoneNumber)
await waitFor(element(by.text('Надіслати код повторно')))
.toBeVisible()
.withTimeout(config.initialTimerCount * 1000)
await element(by.text('Надіслати код повторно')).tap()
await expect(element(by.id('timer'))).toBeVisible()
})
it('Should not go further and show error message when trying to sign in with invalid phone number (not exists in DB))', async () => {
await insertPhoneAndEnter('123456789012')
await expect(element(by.text('Невірно введені дані'))).toBeVisible()
})
it('Should success sign in the app with default phone number (need no confirmation)', async () => {
await insertPhoneAndEnter(config.defaultPhoneNumber)
await expect(element(by.id('homeScreen'))).toExist()
})
it('Should success sign in the app with phone number and correct OTP', async () => {
if (device.getPlatform() === 'ios')
await device.launchApp({
permissions: { camera: 'YES', notifications: 'YES' },
newInstance: true,
delete: true,
})
await insertPhoneAndEnter(config.phoneNumber)
await element(by.id('signInCodeInput')).typeText('1234')
await expect(element(by.id('homeScreen'))).toExist()
})
})

22
e2e/snippets.js

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
export const insertPhoneAndEnter = async (phoneNumber: string) => {
await element(by.id('signInInput')).typeText(phoneNumber)
await element(by.text('Вхід')).tap()
await element(by.id('signInBtn')).tap()
}
export const goToProfileScreen = async () => {
try {
await element(by.id('confirmModalCancelBtn')).tap()
} catch (e) {}
await element(by.id('settingsNavBtn')).tap()
await element(by.text('Профіль користувача')).tap()
}
export const goToCreateTaskScreen = async () => {
try {
await element(by.id('confirmModalCancelBtn')).tap()
} catch (e) {}
await element(by.id('addTaskNavBtn')).tap()
}

26
metro.config.js

@ -5,13 +5,21 @@ @@ -5,13 +5,21 @@
* @format
*/
const defaultSourceExts =
require('metro-config/src/defaults/defaults').sourceExts
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};
resolver: {
sourceExts: process.env.RN_SRC_EXT
? [process.env.RN_SRC_EXT, ...defaultSourceExts]
: defaultSourceExts,
},
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
}

63384
package-lock.json generated

File diff suppressed because it is too large Load Diff

261
package.json

@ -1,130 +1,135 @@ @@ -1,130 +1,135 @@
{
"name": "taskme2",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"postinstall": "react-native setup-ios-permissions && pod-install",
"build:ios": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios'",
"build:android": "cd android && ./gradlew assembleRelease",
"build:android:prod": "cd android && ./gradlew bundleRelease"
},
"dependencies": {
"@bitalikrty/redux-create-reducer": "^1.0.0",
"@gorhom/bottom-sheet": "^4.4.7",
"@miblanchard/react-native-slider": "^2.1.0",
"@notifee/react-native": "^7.8.0",
"@react-native-async-storage/async-storage": "^1.18.2",
"@react-native-camera-roll/camera-roll": "^5.7.2",
"@react-native-community/clipboard": "^1.5.1",
"@react-native-community/netinfo": "^9.3.10",
"@react-native-picker/picker": "^2.4.10",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"@react-navigation/stack": "^6.3.16",
"@tomlangan/react-native-sound-level": "^1.2.1",
"axios": "^1.4.0",
"buffer": "^6.0.3",
"cachios": "^4.0.0",
"deprecated-react-native-prop-types": "^4.1.0",
"events": "^3.3.0",
"ffmpeg-kit-react-native": "^5.1.0",
"jet-tools": "^1.3.0",
"lodash": "^4.17.21",
"mime": "^3.0.0",
"moment": "^2.29.4",
"path": "^0.12.7",
"react": "18.2.0",
"react-native": "0.71.9",
"react-native-app-badge": "^0.1.5",
"react-native-audio-recorder-player": "^3.5.3",
"react-native-autolink": "^4.1.0",
"react-native-calendars": "^1.1298.0",
"react-native-controlled-mentions": "^2.2.5",
"react-native-date-picker": "^4.2.13",
"react-native-device-info": "^10.6.0",
"react-native-document-picker": "^9.0.1",
"react-native-draggable-switch": "^1.1.1",
"react-native-drawer": "^2.5.1",
"react-native-exception-handler": "^2.10.10",
"react-native-expire-storage": "^0.0.3",
"react-native-fast-image": "^8.6.3",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.11.0",
"react-native-gifted-chat": "^2.4.0",
"react-native-html-to-pdf": "^0.12.0",
"react-native-image-crop-picker": "^0.40.0",
"react-native-image-picker": "^5.6.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-masked-text": "^1.13.0",
"react-native-modal": "^13.0.1",
"react-native-onesignal": "^4.5.1",
"react-native-pager-view": "^6.2.0",
"react-native-permissions": "^3.8.4",
"react-native-raw-bottom-sheet": "^2.2.0",
"react-native-reanimated": "^3.4.1",
"react-native-restart": "^0.0.27",
"react-native-safe-area-context": "^4.7.1",
"react-native-screens": "^3.23.0",
"react-native-shadow-2": "^7.0.8",
"react-native-share": "^9.2.3",
"react-native-splash-screen": "^3.3.0",
"react-native-sqlite-storage": "^6.0.1",
"react-native-svg": "^13.10.0",
"react-native-swiper": "^1.6.0",
"react-native-tab-view": "^3.5.2",
"react-native-vector-icons": "^10.0.0",
"react-native-video": "^5.2.1",
"react-native-video-controls": "^2.8.1",
"react-native-webrtc": "^111.0.1",
"react-native-wheel-pick": "^1.2.2",
"react-redux": "^8.1.2",
"redux": "^4.2.1",
"rn-fetch-blob": "^0.12.0",
"socket.io-client": "^4.5.0",
"validate.js": "^0.13.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@react-native-community/eslint-config": "^3.2.0",
"@tsconfig/react-native": "^2.0.2",
"@types/jest": "^29.2.1",
"@types/react": "^18.0.24",
"@types/react-native-drawer": "^2.5.5",
"@types/react-native-sqlite-storage": "^6.0.0",
"@types/react-native-video": "^5.0.14",
"@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.2.1",
"babel-plugin-inline-import": "^3.0.0",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"eslint": "^8.19.0",
"jest": "^29.2.1",
"metro-react-native-babel-preset": "0.73.9",
"pod-install": "^0.1.38",
"prettier": "^2.4.1",
"react-native-make": "^1.0.1",
"react-test-renderer": "18.2.0",
"reactotron-react-native": "^5.0.3",
"reactotron-redux": "^3.1.3",
"typescript": "4.8.4"
},
"jest": {
"preset": "react-native"
},
"reactNativePermissionsIOS": [
"Camera",
"Contacts",
"MediaLibrary",
"Microphone",
"Notifications",
"PhotoLibrary"
]
"name": "taskme2",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"postinstall": "react-native setup-ios-permissions && pod-install",
"build:ios": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios'",
"build:android": "cd android && ./gradlew assembleRelease",
"build:android:prod": "cd android && ./gradlew bundleRelease",
"start:test": "RN_SRC_EXT=e2e.js react-native start",
"test:debug:ios": "detox test --configuration ios.sim.debug",
"test:release:ios": "detox test --configuration ios.sim.release",
"test:debug:android": "detox test --configuration android.emu.debug",
"test:release:android": "detox test --configuration android.emu.release"
},
"dependencies": {
"@bitalikrty/redux-create-reducer": "^1.0.0",
"@gorhom/bottom-sheet": "^4.4.7",
"@miblanchard/react-native-slider": "^2.1.0",
"@notifee/react-native": "^7.8.0",
"@react-native-async-storage/async-storage": "^1.18.2",
"@react-native-camera-roll/camera-roll": "^5.7.2",
"@react-native-community/clipboard": "^1.5.1",
"@react-native-community/netinfo": "^9.3.10",
"@react-native-picker/picker": "^2.4.10",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"@react-navigation/stack": "^6.3.16",
"@tomlangan/react-native-sound-level": "^1.2.1",
"axios": "^1.4.0",
"buffer": "^6.0.3",
"cachios": "^4.0.0",
"deprecated-react-native-prop-types": "^4.1.0",
"events": "^3.3.0",
"ffmpeg-kit-react-native": "^5.1.0",
"jet-tools": "^1.3.0",
"lodash": "^4.17.21",
"mime": "^3.0.0",
"moment": "^2.29.4",
"path": "^0.12.7",
"react": "18.2.0",
"react-native": "0.71.9",
"react-native-app-badge": "^0.1.5",
"react-native-audio-recorder-player": "^3.5.3",
"react-native-autolink": "^4.1.0",
"react-native-calendars": "^1.1298.0",
"react-native-controlled-mentions": "^2.2.5",
"react-native-date-picker": "^4.2.13",
"react-native-device-info": "^10.6.0",
"react-native-document-picker": "^9.0.1",
"react-native-draggable-switch": "^1.1.1",
"react-native-drawer": "^2.5.1",
"react-native-exception-handler": "^2.10.10",
"react-native-expire-storage": "^0.0.3",
"react-native-fast-image": "^8.6.3",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.11.0",
"react-native-gifted-chat": "^2.4.0",
"react-native-html-to-pdf": "^0.12.0",
"react-native-image-crop-picker": "^0.40.0",
"react-native-image-picker": "^5.6.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-masked-text": "^1.13.0",
"react-native-modal": "^13.0.1",
"react-native-onesignal": "^4.5.1",
"react-native-pager-view": "^6.2.0",
"react-native-permissions": "^3.8.4",
"react-native-raw-bottom-sheet": "^2.2.0",
"react-native-reanimated": "^3.4.1",
"react-native-restart": "^0.0.27",
"react-native-safe-area-context": "^4.7.1",
"react-native-screens": "^3.23.0",
"react-native-shadow-2": "^7.0.8",
"react-native-share": "^9.2.3",
"react-native-splash-screen": "^3.3.0",
"react-native-sqlite-storage": "^6.0.1",
"react-native-svg": "^13.10.0",
"react-native-swiper": "^1.6.0",
"react-native-tab-view": "^3.5.2",
"react-native-vector-icons": "^10.0.0",
"react-native-video": "^5.2.1",
"react-native-video-controls": "^2.8.1",
"react-native-webrtc": "^111.0.1",
"react-native-wheel-pick": "^1.2.2",
"react-redux": "^8.1.2",
"redux": "^4.2.1",
"rn-fetch-blob": "^0.12.0",
"socket.io-client": "^4.5.0",
"validate.js": "^0.13.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@react-native-community/eslint-config": "^3.2.0",
"@tsconfig/react-native": "^2.0.2",
"@types/jest": "^29.2.1",
"@types/react": "^18.0.24",
"@types/react-native-drawer": "^2.5.5",
"@types/react-native-sqlite-storage": "^6.0.0",
"@types/react-native-video": "^5.0.14",
"@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.2.1",
"babel-plugin-inline-import": "^3.0.0",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"detox": "^20.11.4",
"eslint": "^8.19.0",
"jest": "^29.2.1",
"metro-react-native-babel-preset": "0.73.9",
"pod-install": "^0.1.38",
"prettier": "^2.4.1",
"react-native-make": "^1.0.1",
"react-test-renderer": "18.2.0",
"reactotron-react-native": "^5.0.3",
"reactotron-redux": "^3.1.3",
"typescript": "4.8.4"
},
"jest": {
"preset": "react-native"
},
"reactNativePermissionsIOS": [
"Camera",
"Contacts",
"MediaLibrary",
"Microphone",
"Notifications",
"PhotoLibrary"
]
}

BIN
src/assets/images/default_avatar.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

17
src/config/index.ts

@ -2,12 +2,24 @@ import { fonts } from './fonts' @@ -2,12 +2,24 @@ import { fonts } from './fonts'
/**
* Dev
*/
export const testData = {
defaultPhoneNumber: '380000000000', // prod
phoneNumber: '380980717900', // prod
initialTimerCount: 120, //prod
// defaultPhoneNumber: '380980717970', // dev
// phoneNumber: '380990112233', // dev
// initialTimerCount: 10, //dev
existEmail: 'admin@admin.com',
}
export const dynamicConfig = {
// baseUrl: 'http://localhost:3000',
// socketUrl: 'http://localhost:3000',
// baseUrl: 'https://5763-46-63-4-20.ngrok.io',
// socketUrl: 'https://5763-46-63-4-20.ngrok.io',
// baseUrl: 'https://3a67-46-63-4-20.ngrok.io',
// socketUrl: 'https://3a67-46-63-4-20.ngrok.io',
// baseUrl: 'http://46.101.170.206:5000/app/',
// socketUrl: 'http://46.101.170.206:5000',
@ -30,5 +42,6 @@ export const dynamicConfig = { @@ -30,5 +42,6 @@ export const dynamicConfig = {
export const config = {
...dynamicConfig,
...testData,
fonts,
}

1
src/modules/account/atoms/edit-avatar.atom.tsx

@ -43,6 +43,7 @@ export const EditAvatarAtom: FC<IEditAvatarAtomProps> = ({ @@ -43,6 +43,7 @@ export const EditAvatarAtom: FC<IEditAvatarAtomProps> = ({
</View>
{avatarUrl || preview ? (
<TouchableOpacity
testID="clearAvatarBtn"
style={styles.removePhotoBtn}
onPress={() => onPressRemove()}>
<IconComponent

10
src/modules/account/components/account-phone-number-input.component.tsx

@ -15,6 +15,7 @@ interface IProps { @@ -15,6 +15,7 @@ interface IProps {
error?: string
disable?: boolean
iconName?: string
testID?: string
}
export const AccountPhoneNumberInput = (props: IProps) => {
@ -26,6 +27,7 @@ export const AccountPhoneNumberInput = (props: IProps) => { @@ -26,6 +27,7 @@ export const AccountPhoneNumberInput = (props: IProps) => {
<Txt style={[styles.label]}>{props.label}</Txt>
<View style={onCls(Boolean(props.error), 'inputWrap')}>
<MaskedInput
testID={props.testID}
type={'cel-phone'}
value={props.value}
name={props.name}
@ -45,7 +47,13 @@ export const AccountPhoneNumberInput = (props: IProps) => { @@ -45,7 +47,13 @@ export const AccountPhoneNumberInput = (props: IProps) => {
/>
</View>
{props.error && <Text style={styles.error}>{props.error}</Text>}
{props.error && (
<Text
testID={props.testID ? `${props.testID}Error` : null}
style={styles.error}>
{props.error}
</Text>
)}
</View>
</>
)

4
src/modules/account/components/change-date-of-birthday-moda.component.tsx

@ -32,8 +32,9 @@ export const ChangeDateOfBirthdayModal: FC<IProps> = ({ @@ -32,8 +32,9 @@ export const ChangeDateOfBirthdayModal: FC<IProps> = ({
height={Platform.OS === 'ios' ? $size(400) : $size(350)}
title={title}
sheetRef={sheetRef}>
<View style={styles.container}>
<View style={styles.container} testID="datePickerView">
<DatePicker
testID="datePicker"
textColor={theme.$textPrimary}
date={newBirthdayDate}
is24hourSource="locale"
@ -44,6 +45,7 @@ export const ChangeDateOfBirthdayModal: FC<IProps> = ({ @@ -44,6 +45,7 @@ export const ChangeDateOfBirthdayModal: FC<IProps> = ({
maximumDate={new Date()}
/>
<Button
testID="selectDateBtn"
title="Застосувати"
type="primary"
onPress={() => onChange(newBirthdayDate)}

2
src/modules/account/components/change-phone-number-modal.component.tsx

@ -39,6 +39,7 @@ export const ChangePhoneNumberModal = ({ @@ -39,6 +39,7 @@ export const ChangePhoneNumberModal = ({
sheetRef={sheetRef}>
<View style={styles.container}>
<FormPhone
testID="workPhoneInput"
name="newPhoneNumber"
value={newPhoneNumber}
onChange={val => setNewPhoneNumber(val)}
@ -47,6 +48,7 @@ export const ChangePhoneNumberModal = ({ @@ -47,6 +48,7 @@ export const ChangePhoneNumberModal = ({
/>
<Button
testID="changeWorkPhoneBtn"
title="Продовжити"
type="primary"
onPress={() => onSubmit(newPhoneNumber)}

3
src/modules/account/components/fake-date-input-with-modal.component.tsx

@ -13,6 +13,7 @@ interface IProps { @@ -13,6 +13,7 @@ interface IProps {
onChange: (newDateOfBirthday: Date) => void
style: ViewStyle
error?: string
testID?: string
}
export const FakeDateInputWithModal: FC<IProps> = ({
@ -20,6 +21,7 @@ export const FakeDateInputWithModal: FC<IProps> = ({ @@ -20,6 +21,7 @@ export const FakeDateInputWithModal: FC<IProps> = ({
style,
onChange,
error,
testID,
}) => {
const { styles } = useTheme(createStyles)
@ -32,6 +34,7 @@ export const FakeDateInputWithModal: FC<IProps> = ({ @@ -32,6 +34,7 @@ export const FakeDateInputWithModal: FC<IProps> = ({
return (
<View style={[styles.container, style]}>
<TouchableFakeInput
testID={testID}
value={dateToRender}
label={'День народження'}
iconName={'cake-1'}

1
src/modules/account/components/work-phone-number.component.tsx

@ -75,6 +75,7 @@ export const WorkPhoneNumber = () => { @@ -75,6 +75,7 @@ export const WorkPhoneNumber = () => {
return (
<View style={styles.container}>
<TouchableFakeInput
testID="workPhone"
label="Тел. робочий"
value={phoneNumber}
iconName="phone-1"

4
src/modules/account/hooks/use-account-editor.hook.ts

@ -128,7 +128,9 @@ export const useAccountEditor = () => { @@ -128,7 +128,9 @@ export const useAccountEditor = () => {
onPressOk: () => {},
})
} catch (e: any) {
const message = e.response?.data?.key ? getMessageByExceptionKey(e.response?.data?.key) : 'Спробуйте будь-ласка пізніше.'
const message = e.response?.data?.key
? getMessageByExceptionKey(e.response?.data?.key)
: 'Спробуйте будь-ласка пізніше.'
appEvents.emit('openInfoModal', {
title: 'Сталась помилка!',
message,

12
src/modules/account/screens/account.screen.tsx

@ -64,6 +64,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -64,6 +64,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
return (
<ScreenLayout
needScroll
testID="profileScreen"
scrollRef={scrollRef}
keyboardSpacerOn={false}
extraHeight={$size(180, 180)}
@ -78,6 +79,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -78,6 +79,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
onPress={Keyboard.dismiss}>
<View style={styles.container}>
<ImageCropPicker
testID="profileAvatar"
style={styles.imageContainer}
onChange={image => setFormField('avatar', image)}>
{(preview, onPressRemove) => (
@ -102,6 +104,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -102,6 +104,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
/>
<FakeDateInputWithModal
testID="profileDOBInput"
style={styles.dateOfBirthField}
date={values.dateOfBirth}
onChange={v => setFormField('dateOfBirth', v)}
@ -109,6 +112,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -109,6 +112,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
/>
<TextInputWithIcon
testID="profileNameInput"
title="Імя"
value={values.firstName}
iconName="usersquare-1"
@ -117,6 +121,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -117,6 +121,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
/>
<TextInputWithIcon
testID="profileLastNameInput"
title="Прізвище"
value={values.lastName}
iconName="usersquare-1"
@ -125,6 +130,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -125,6 +130,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
/>
<TextInputWithIcon
testID="profileMiddleNameInput"
title="По-батькові"
value={values.middleName}
iconName="usersquare-1"
@ -133,6 +139,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -133,6 +139,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
/>
<TextInputWithIcon
testID="profilePositionInput"
title="Посада"
value={values.position}
iconName="usersquare-1"
@ -143,6 +150,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -143,6 +150,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
<WorkPhoneNumber />
<AccountPhoneNumberInput
testID="profilePersonalPhoneInput"
name="personalPhone"
label="Тел. особистий"
value={values.personalPhoneNumber}
@ -152,6 +160,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -152,6 +160,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
/>
<TextInputWithIcon
testID="profileInnerPhoneInput"
title="Тел. внутрішній"
value={values.innerPhoneNumber}
iconName="phone-1"
@ -165,6 +174,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -165,6 +174,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
/>
<TextInputWithIcon
testID="profileEmailInput"
title="E-mail"
value={values.email}
iconName="email"
@ -184,6 +194,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -184,6 +194,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
)}
<Button
testID="profileSaveBtn"
style={styles.btn}
title="Зберегти зміни"
type="primary"
@ -191,6 +202,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => { @@ -191,6 +202,7 @@ export const AccountScreen: FC<IProps> = ({ navigation }) => {
showLoadingIndicator={isLoading}
/>
<Button
testID="profileLogoutBtn"
style={styles.btn}
title="Вийти з профілю"
type="border"

7
src/modules/auth/screens/confirm-code.screen.tsx

@ -22,8 +22,9 @@ import { @@ -22,8 +22,9 @@ import {
} from '@/shared'
import { useAuthorization } from '../hooks'
import { PartialTheme } from '@/shared/themes/interfaces'
import { config } from '@/config'
const initialCount = 120
const initialCount = config.initialTimerCount || 120
export const ConfirmCode: FC = () => {
const [isDisabledField, setField] = useState<boolean>(false)
@ -77,7 +78,7 @@ export const ConfirmCode: FC = () => { @@ -77,7 +78,7 @@ export const ConfirmCode: FC = () => {
</TouchableOpacity>
)
return (
<Text style={styles.inputTitle}>
<Text style={styles.inputTitle} testID="timer">
Код застаріє через{' '}
<Text style={styles.timer}>{getformattedTimer()}</Text>
</Text>
@ -116,6 +117,7 @@ export const ConfirmCode: FC = () => { @@ -116,6 +117,7 @@ export const ConfirmCode: FC = () => {
<Txt style={styles.label}>Вхід</Txt>
<LargeFormControlWithIcon
testID="signInCodeInput"
disabled={isDisabledField}
title={renderTitle()}
name={'verifyCode'}
@ -135,6 +137,7 @@ export const ConfirmCode: FC = () => { @@ -135,6 +137,7 @@ export const ConfirmCode: FC = () => {
title="Підтвердити"
onPress={() => confirmLogin(confirmCode)}
showLoadingIndicator={isLoading}
testID="sendCodeBtn"
/>
</View>
</View>

4
src/modules/auth/screens/sign-in.screen.tsx

@ -28,6 +28,7 @@ export const SignInScreen: FC<IProps> = ({ navigation }) => { @@ -28,6 +28,7 @@ export const SignInScreen: FC<IProps> = ({ navigation }) => {
return (
<ScreenLayout
testID="signInScreen"
needScroll={true}
keyboardSpacerOn={false}
scrollRef={scrollRef}>
@ -40,6 +41,7 @@ export const SignInScreen: FC<IProps> = ({ navigation }) => { @@ -40,6 +41,7 @@ export const SignInScreen: FC<IProps> = ({ navigation }) => {
source={require('@/assets/images/auth_1.png')}
height={$size(200)}
width={$size(200)}
testID="signInImage"
/>
<View>
@ -51,6 +53,7 @@ export const SignInScreen: FC<IProps> = ({ navigation }) => { @@ -51,6 +53,7 @@ export const SignInScreen: FC<IProps> = ({ navigation }) => {
onChange={v => setPhoneNumber(v)}
value={phoneNumber}
error={error}
testID="signInInput"
/>
<Button
@ -58,6 +61,7 @@ export const SignInScreen: FC<IProps> = ({ navigation }) => { @@ -58,6 +61,7 @@ export const SignInScreen: FC<IProps> = ({ navigation }) => {
title="Підтвердити"
onPress={() => sendCode(phoneNumber)}
showLoadingIndicator={isLoading}
testID="signInBtn"
/>
</View>
</View>

1
src/modules/home/screens/home.screen.tsx

@ -91,6 +91,7 @@ export const HomeScreen: FC<IProps> = ({ navigation }) => { @@ -91,6 +91,7 @@ export const HomeScreen: FC<IProps> = ({ navigation }) => {
return (
<ScreenLayout
testID="homeScreen"
horizontalPadding={0}
filter={{
isOpenDrawer,

5
src/modules/root/atoms/icon-with-count-indicator.component.tsx

@ -11,6 +11,7 @@ interface IProps { @@ -11,6 +11,7 @@ interface IProps {
route: string
indicatorCount: number
onPress: () => void
testID?: string
}
export const IconWithCountIndicator: FC<IProps> = ({
@ -19,12 +20,12 @@ export const IconWithCountIndicator: FC<IProps> = ({ @@ -19,12 +20,12 @@ export const IconWithCountIndicator: FC<IProps> = ({
route,
indicatorCount = 99,
onPress,
testID,
}) => {
const {
styles,
theme: { tabBar, $layoutBg },
} = useTheme(createStyles)
const isAddTask = route === RouteKey.AddTask
if (isAddTask && Platform.OS === 'android') {
@ -40,6 +41,7 @@ export const IconWithCountIndicator: FC<IProps> = ({ @@ -40,6 +41,7 @@ export const IconWithCountIndicator: FC<IProps> = ({
onPress={onPress}
style={[styles.item, styles.navItemAddTask]}>
<IconComponent
testID={testID}
name={iconName}
size={$size(24, 22)}
style={[
@ -75,6 +77,7 @@ export const IconWithCountIndicator: FC<IProps> = ({ @@ -75,6 +77,7 @@ export const IconWithCountIndicator: FC<IProps> = ({
<View style={styles.container}>
<IconComponent
testID={testID}
name={iconName}
size={$size(24, 22)}
style={[

4
src/modules/root/components/tab-bar.component.tsx

@ -24,12 +24,16 @@ export const TabBarComponent = (props: ITabBarProps) => { @@ -24,12 +24,16 @@ export const TabBarComponent = (props: ITabBarProps) => {
const items = props.items.map((route, index) => {
const isActive = props.activeIndex === index
const onPress = () => props.onPressItem(index, route)
return (
<IconWithCountIndicator
key={`${route}---${index}`}
iconName={getTabBarIconsConfig()[route].iconName}
isActive={isActive}
route={route}
testID={`${route.charAt(0).toLowerCase()}${route.slice(
1,
)}NavBtn`}
indicatorCount={
getTabBarIconsConfig({
home: unreadTasksCount,

2
src/modules/root/smart-components/confirm-modal.smart-component.tsx

@ -88,6 +88,7 @@ export const ConfirmModalSmartComponent: FC = () => { @@ -88,6 +88,7 @@ export const ConfirmModalSmartComponent: FC = () => {
<View style={styles.btnsPair}>
<Button
testID="confirmModalCancelBtn"
title={'Ні'}
onPress={() => pressNowAllow()}
style={styles.btn}
@ -99,6 +100,7 @@ export const ConfirmModalSmartComponent: FC = () => { @@ -99,6 +100,7 @@ export const ConfirmModalSmartComponent: FC = () => {
/>
<Button
title={'Так'}
testID="confirmModalOkBtn"
onPress={() => pressAllow()}
style={styles.btn}
type={

3
src/modules/root/smart-components/date-picker.smart-component.tsx

@ -79,8 +79,9 @@ export const DatePickerSmartComponent: FC = () => { @@ -79,8 +79,9 @@ export const DatePickerSmartComponent: FC = () => {
keyboardBlurBehavior="restore"
onDismiss={onClose}
handleIndicatorStyle={[styles.indicator]}>
<View style={styles.wrapper}>
<View style={styles.wrapper} testID="datePickerView">
<DatePicker
testID="datePicker"
date={date}
mode={settingsRef.current.mode}
textColor={theme?.$textPrimary}

5
src/modules/root/smart-components/info-modal.smart-component.tsx

@ -68,9 +68,12 @@ export const InfoModalSmartComponent: FC = () => { @@ -68,9 +68,12 @@ export const InfoModalSmartComponent: FC = () => {
onClose={settingsRef?.current?.onPressOk}
title={title}>
{isLoading ? <Loading /> : null}
<Txt style={styles.message}>{message}</Txt>
<Txt testID="infoModalTxt" style={styles.message}>
{message}
</Txt>
{buttonText === 'false' ? null : (
<Button
testID="infoModalBtn"
title={buttonText}
onPress={() => {
sheetRef.current.close()

14
src/modules/tasks/atoms/add-task-row.atom.tsx

@ -11,6 +11,7 @@ interface IProps { @@ -11,6 +11,7 @@ interface IProps {
onPressDelIcon?: () => void
border?: boolean
error?: string
testID?: string
}
export const AddTaskRow: FC<IProps> = ({
@ -20,6 +21,7 @@ export const AddTaskRow: FC<IProps> = ({ @@ -20,6 +21,7 @@ export const AddTaskRow: FC<IProps> = ({
onPress,
onPressDelIcon,
border = true,
testID,
}) => {
const { styles, theme } = useTheme(createStyles)
@ -32,7 +34,9 @@ export const AddTaskRow: FC<IProps> = ({ @@ -32,7 +34,9 @@ export const AddTaskRow: FC<IProps> = ({
{rightComponent?.text ? (
<View style={styles.rigthBlock}>
<TouchableOpacity onPress={onPress}>
<Txt style={styles.text}>{rightComponent.text}</Txt>
<Txt style={styles.text} testID={testID}>
{rightComponent.text}
</Txt>
</TouchableOpacity>
{onPressDelIcon ? (
<RoundButton
@ -59,7 +63,13 @@ export const AddTaskRow: FC<IProps> = ({ @@ -59,7 +63,13 @@ export const AddTaskRow: FC<IProps> = ({
)}
</Pressable>
{error ? <Txt style={styles.error}>{error}</Txt> : null}
{error ? (
<Txt
style={styles.error}
testID={testID ? `${testID}Error` : null}>
{error}
</Txt>
) : null}
</>
)
}

1
src/modules/tasks/atoms/create-task-description.atom.tsx

@ -20,6 +20,7 @@ export const CreateTaskDescription: FC<IProps> = ({ @@ -20,6 +20,7 @@ export const CreateTaskDescription: FC<IProps> = ({
return (
<FormTextarea
testID="taskDescription"
name="description"
label="Опис"
inputStyle={styles.input}

1
src/modules/tasks/atoms/create-task-title-field.atom.tsx

@ -18,6 +18,7 @@ export const CreateTaskTitleField: FC<IProps> = ({ @@ -18,6 +18,7 @@ export const CreateTaskTitleField: FC<IProps> = ({
return (
<FormTextarea
testID="taskTitle"
name="title"
label="Назва"
inputStyle={styles.input}

8
src/modules/tasks/components/add-new-reason-modal.component.tsx

@ -38,6 +38,7 @@ export const AddNewReasonModal: FC<IProps> = ({ @@ -38,6 +38,7 @@ export const AddNewReasonModal: FC<IProps> = ({
onClose={() => setKeyboardScrollAwareOptions(true)}>
<View style={styles.container}>
<LargeFormControlWithIcon
testID="reasonInput"
name="reasonName"
value={reasonName}
maxLength={40}
@ -48,11 +49,16 @@ export const AddNewReasonModal: FC<IProps> = ({ @@ -48,11 +49,16 @@ export const AddNewReasonModal: FC<IProps> = ({
/>
<Button
testID="savePersonalReasonBtn"
title={'Зберегти одноразово'}
onPress={() => onSubmit(true)}
style={styles.btn}
/>
<Button title={'Зберегти в довідник'} onPress={onSubmit} />
<Button
testID="saveCommonReasonBtn"
title={'Зберегти в довідник'}
onPress={onSubmit}
/>
</View>
</BottomModal>
)

8
src/modules/tasks/components/reason-form.component.tsx

@ -23,7 +23,6 @@ interface IProps { @@ -23,7 +23,6 @@ interface IProps {
export const ReasonForm: FC<IProps> = ({
activeReason,
reasonsToOptions,
showLabel = true,
error,
setActiveReason,
@ -47,12 +46,17 @@ export const ReasonForm: FC<IProps> = ({ @@ -47,12 +46,17 @@ export const ReasonForm: FC<IProps> = ({
{showLabel ? <Txt style={styles.blockTitle}>Підстава</Txt> : null}
<View style={styles.select}>
<FormSelect
testID="reasonSelect"
title={activeReason?.name || 'Оберіть зі списку'}
buttonStyle={styles.button}
onPress={openSelectReasonModal}
/>
{error ? <Txt style={styles.error}>{error}</Txt> : null}
{error ? (
<Txt style={styles.error} testID="reasonSelectError">
{error}
</Txt>
) : null}
<AddNewReasonSmart
reasonName={newReasonName}

8
src/modules/tasks/components/set-task-interval.component.tsx

@ -35,6 +35,7 @@ export const SetTaskInterval: FC<IProps> = ({ @@ -35,6 +35,7 @@ export const SetTaskInterval: FC<IProps> = ({
<>
<View style={styles.container}>
<FakeDateInputForm
testID="taskStartDate"
value={startDate.date}
onPress={() => openDateModal('startDate', startDate)}
title={'Початок'}
@ -42,6 +43,7 @@ export const SetTaskInterval: FC<IProps> = ({ @@ -42,6 +43,7 @@ export const SetTaskInterval: FC<IProps> = ({
/>
<FakeDateInputForm
testID="taskEndDate"
value={endDate.date}
onPress={() => openDateModal('endDate', endDate)}
title={'Кінець'}
@ -49,7 +51,11 @@ export const SetTaskInterval: FC<IProps> = ({ @@ -49,7 +51,11 @@ export const SetTaskInterval: FC<IProps> = ({
/>
</View>
{error && <Text style={styles.error}>{error}</Text>}
{error && (
<Text style={styles.error} testID="taskDateError">
{error}
</Text>
)}
</>
)
}

3
src/modules/tasks/screens/add-update-task.screen.tsx

@ -192,6 +192,7 @@ export const AddUpdateTaskScreen: FC<IProps> = ({ route, navigation }) => { @@ -192,6 +192,7 @@ export const AddUpdateTaskScreen: FC<IProps> = ({ route, navigation }) => {
return (
<ScreenLayout
testID="editTaskScreen"
needScroll
scrollRef={screenRef}
header={{
@ -248,6 +249,7 @@ export const AddUpdateTaskScreen: FC<IProps> = ({ route, navigation }) => { @@ -248,6 +249,7 @@ export const AddUpdateTaskScreen: FC<IProps> = ({ route, navigation }) => {
)}
<AddTaskRow
testID="taskInitiator"
onPress={openSelectInitiatorModal}
onPressDelIcon={clearInitiatorField}
title={'Ініціатор'}
@ -306,6 +308,7 @@ export const AddUpdateTaskScreen: FC<IProps> = ({ route, navigation }) => { @@ -306,6 +308,7 @@ export const AddUpdateTaskScreen: FC<IProps> = ({ route, navigation }) => {
/>
<Button
testID="saveTaskBtn"
title={buttonTitle}
type={'primary'}
style={styles.submitBtn}

1
src/modules/tasks/smart-components/add-new-reason.smart-component.tsx

@ -22,6 +22,7 @@ export const AddNewReasonSmart: FC<IProps> = ({ @@ -22,6 +22,7 @@ export const AddNewReasonSmart: FC<IProps> = ({
return (
<>
<FormSelect
testID="addReasonInput"
title="Додайте нову"
icon={{ name: 'add-file', size: $size(16, 14) }}
buttonStyle={styles.button}

1
src/modules/tasks/smart-components/select-task-executor-list.smart-component.tsx

@ -47,6 +47,7 @@ export const SelectTaskExecutorListSmart: FC<IProps> = ({ @@ -47,6 +47,7 @@ export const SelectTaskExecutorListSmart: FC<IProps> = ({
return (
<>
<AddTaskRow
testID="taskExecutor"
title={'Виконавець'}
rightComponent={{ icon: 'plus-1' }}
onPress={() =>

29
src/providers/ImageCropPickerOpenPickerProvider.e2e.js

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
import { fsService } from '@/services/system'
import { base64AvatarString } from '@/shared/consts/base64Avatar'
import { base64BGString } from '@/shared/consts/base64Background'
import { Platform } from 'react-native'
async function openPicker(options) {
const filePath = await fsService.writeFile(
{
content: Platform.select({
ios: base64AvatarString,
default: base64BGString,
}),
fileName: 'Test_avatar.png',
},
'base64',
)
const path = 'file://' + filePath
return {
filename: 'Test_avatar.png',
path,
height: 200,
width: 200,
mime: 'image/png',
size: 500,
}
}
export default openPicker

2
src/providers/ImageCropPickerOpenPickerProvider.js

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
import { openPicker } from 'react-native-image-crop-picker'
export default openPicker

7
src/repositories/api/account.repository.ts

@ -28,7 +28,12 @@ export class ApiAccountRepository extends ApiRepository { @@ -28,7 +28,12 @@ export class ApiAccountRepository extends ApiRepository {
return this.http.put<{ avatarUrl: string }>(
'account/avatar',
convertToFormData(avatar),
{ timeout: 30000 },
{
headers: {
'Content-Type': 'multipart/form-data',
timeout: 30000,
},
},
)
}

2
src/repositories/api/chat.repository.ts

@ -45,6 +45,7 @@ export class ApiChatRepository extends ApiRepository { @@ -45,6 +45,7 @@ export class ApiChatRepository extends ApiRepository {
return this.http.post<NewChatGroupId>(
'chats/group-chat',
convertToFormData(params),
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
}
@ -52,6 +53,7 @@ export class ApiChatRepository extends ApiRepository { @@ -52,6 +53,7 @@ export class ApiChatRepository extends ApiRepository {
return this.http.patch<void>(
`chats/${payload.chatId}`,
convertToFormData(payload.data),
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
}

4
src/services/system/fs.service.ts

@ -48,7 +48,7 @@ const getFileStat = async (filePath: string): Promise<IFile> => { @@ -48,7 +48,7 @@ const getFileStat = async (filePath: string): Promise<IFile> => {
}
}
const writeFile = (data: IWriteFileData) => {
const writeFile = (data: IWriteFileData, encoding = 'utf8') => {
return new Promise<string>((resolve, reject) => {
const directory = isAndroid(
RNFS.DownloadDirectoryPath,
@ -56,7 +56,7 @@ const writeFile = (data: IWriteFileData) => { @@ -56,7 +56,7 @@ const writeFile = (data: IWriteFileData) => {
)
const path = directory + `/${data.fileName}`
RNFS.writeFile(path, data.content, 'utf8')
RNFS.writeFile(path, data.content, encoding)
.then(success => resolve(path))
.catch(err => reject(false))
})

10
src/services/system/media.service.ts

@ -6,6 +6,7 @@ import { mediaPermissionsService } from './media-permissions.service' @@ -6,6 +6,7 @@ import { mediaPermissionsService } from './media-permissions.service'
import DocumentPicker from 'react-native-document-picker'
import { prepareFile, prepareFiles } from '@/shared/helpers'
import AudioRecorderPlayer from 'react-native-audio-recorder-player'
import openPicker from '@/providers/ImageCropPickerOpenPickerProvider'
interface IPickerProps {
width: number
@ -35,6 +36,7 @@ const launchDeviceCamera = async () => { @@ -35,6 +36,7 @@ const launchDeviceCamera = async () => {
const hasPermissions = await mediaPermissionsService.checkCameraPermissions(
permissions.camera,
)
if (!hasPermissions) return null
let video = await ImagePicker.openCamera({
@ -53,7 +55,7 @@ const launchDeviceCamera = async () => { @@ -53,7 +55,7 @@ const launchDeviceCamera = async () => {
return prepareFile(video)
}
const openCamera = async (props?: Options) => {
const openDeviceCamera = async (props?: Options) => {
const hasPermissions = await mediaPermissionsService.checkCameraPermissions(
permissions.camera,
)
@ -87,7 +89,7 @@ const openCropPicker = async ({ @@ -87,7 +89,7 @@ const openCropPicker = async ({
if (!hasPermissions) return null
const img = await ImagePicker.openPicker({
const img = await openPicker({
...props,
cropping: true,
cropperCircleOverlay,
@ -107,7 +109,7 @@ const openMultiplePicker = async () => { @@ -107,7 +109,7 @@ const openMultiplePicker = async () => {
permissions.gallery,
)
if (!hasPermissions) return null
const images: any = await ImagePicker.openPicker({
const images: any = await openPicker({
multiple: true,
includeBase64: false,
mediaType: 'any',
@ -178,7 +180,7 @@ const onStopPlay = async () => { @@ -178,7 +180,7 @@ const onStopPlay = async () => {
}
export const mediaService = {
openCamera,
openCamera: openDeviceCamera,
openCropPicker,
openGalleryPicker,
openMultiplePicker,

31
src/services/system/reactron.service.ts

@ -1,15 +1,28 @@ @@ -1,15 +1,28 @@
import AsyncStorageLib from '@react-native-async-storage/async-storage'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { NativeModules } from 'react-native'
import Reactotron from 'reactotron-react-native'
Reactotron.setAsyncStorageHandler(AsyncStorageLib)
.configure()
.useReactNative()
import { reactotronRedux } from 'reactotron-redux'
const scriptURL = NativeModules.SourceCode.scriptURL
const packagerHostname = scriptURL.split('://')[1].split(':')[0]
Reactotron.setAsyncStorageHandler(AsyncStorage) // AsyncStorage would either come from `react-native` or `@react-native-community/async-storage` depending on where you get it from
.configure({
name: 'React Native Demo',
host: packagerHostname,
})
.useReactNative({
asyncStorage: false, // there are more options to the async storage.
networking: {
// optionally, you can turn it off with false.
ignoreUrls: /symbolicate/,
},
editor: false, // there are more options to editor
errors: { veto: () => false }, // or turn it off with false
overlay: false, // just turning off overlay
})
.use(reactotronRedux())
.connect()
const yeOldeConsoleLog = console.log
console.log = (...props) => {
yeOldeConsoleLog(...props)
Reactotron.log(...props)
}
}

2
src/services/system/real-time.service.ts

@ -58,7 +58,7 @@ export class SocketIo { @@ -58,7 +58,7 @@ export class SocketIo {
}
_disconnect() {
this.socket.disconnect()
this.socket?.disconnect()
}
_connect() {

6
src/shared/components/buttons/button.component.tsx

@ -36,6 +36,11 @@ interface ButtonProps { @@ -36,6 +36,11 @@ interface ButtonProps {
* Show loading indicator
*/
showLoadingIndicator?: boolean
/**
* ID for testing
*/
testID?: string
}
export const Button: FC<ButtonProps> = ({ type = 'primary', ...props }) => {
@ -65,6 +70,7 @@ export const Button: FC<ButtonProps> = ({ type = 'primary', ...props }) => { @@ -65,6 +70,7 @@ export const Button: FC<ButtonProps> = ({ type = 'primary', ...props }) => {
}
return (
<TouchableOpacity
testID={props.testID}
disabled={props.isDisabled || props.showLoadingIndicator}
style={[styles[`${type}Wrap`], styles.basicBtn, props.style]}
onPress={onPress}>

78
src/shared/components/elements/icon.component.tsx

@ -1,45 +1,43 @@ @@ -1,45 +1,43 @@
import React from 'react';
import { createIconSetFromFontello } from 'react-native-vector-icons';
import { fontelloConfig } from '@/config/fontello.config';
import { StyleSheet, TouchableOpacity, ViewStyle } from 'react-native';
const Icon = createIconSetFromFontello(fontelloConfig);
import React from 'react'
import { createIconSetFromFontello } from 'react-native-vector-icons'
import { fontelloConfig } from '@/config/fontello.config'
import { TouchableOpacity, ViewStyle } from 'react-native'
const Icon = createIconSetFromFontello(fontelloConfig)
interface IProps {
name: string
size: number
color?: string
style?: any
onPress?: () => void
btnStyle?: ViewStyle
name: string
size: number
color?: string
style?: any
onPress?: () => void
btnStyle?: ViewStyle
testID?: string
}
export const IconComponent = ({
onPress,
...props
}: IProps) => {
if (onPress) {
return (
<TouchableOpacity onPress={onPress} style={[ props.btnStyle]}>
<Icon
name={props.name}
size={props.size}
color={props.color}
style={props.style}
/>
</TouchableOpacity>
)
} else {
return (
<Icon
name={props.name}
size={props.size}
color={props.color}
style={props.style}
/>
)
}
export const IconComponent = ({ onPress, ...props }: IProps) => {
if (onPress) {
return (
<TouchableOpacity
testID={props.testID}
onPress={onPress}
style={[props.btnStyle]}>
<Icon
name={props.name}
size={props.size}
color={props.color}
style={props.style}
/>
</TouchableOpacity>
)
} else {
return (
<Icon
testID={props.testID}
name={props.name}
size={props.size}
color={props.color}
style={props.style}
/>
)
}
}
const styles = StyleSheet.create({
})

3
src/shared/components/elements/txt.component.tsx

@ -3,7 +3,7 @@ import _ from 'lodash' @@ -3,7 +3,7 @@ import _ from 'lodash'
import { Text, TextProps, TextStyle } from 'react-native'
import { config } from '@/config'
import {getFont} from '@/shared/helpers';
import { getFont } from '@/shared/helpers'
interface TxtProps extends TextProps {
/**
@ -20,6 +20,7 @@ interface TxtProps extends TextProps { @@ -20,6 +20,7 @@ interface TxtProps extends TextProps {
* Text value
*/
children?: string | number
testID?: string
}
export const Txt = (props: TxtProps) => {

3
src/shared/components/forms/form-fake-date-input.component.tsx

@ -19,6 +19,7 @@ interface IProps { @@ -19,6 +19,7 @@ interface IProps {
needIcon?: boolean
iconName?: string
disabled?: boolean
testID?: string
}
export const FakeDateInputForm = ({
@ -45,7 +46,7 @@ export const FakeDateInputForm = ({ @@ -45,7 +46,7 @@ export const FakeDateInputForm = ({
disabled={props.disabled}
/>
)}
<Text style={styles.date}>
<Text style={styles.date} testID={props.testID}>
{props.value &&
moment(new Date(props.value)).format('DD-MM-YYYY')}
{props.timeValue &&

2
src/shared/components/forms/form-large-control-with-icon.component.tsx

@ -42,6 +42,7 @@ interface IProps { @@ -42,6 +42,7 @@ interface IProps {
inputType?: TextInputProps['keyboardType']
maxLength?: number
needDoneBtnAbove?: boolean
testID?: string
}
export const LargeFormControlWithIcon = (props: IProps) => {
@ -81,6 +82,7 @@ export const LargeFormControlWithIcon = (props: IProps) => { @@ -81,6 +82,7 @@ export const LargeFormControlWithIcon = (props: IProps) => {
</View>
)}
<TextInput
testID={props.testID}
inputAccessoryViewID={INPUT_ACCESSORIES_VIEW_ID}
keyboardType={props.inputType}
returnKeyType={'done'}

8
src/shared/components/forms/form-phone.component.tsx

@ -18,6 +18,7 @@ interface IProps { @@ -18,6 +18,7 @@ interface IProps {
labelStyles?: any
inputStyles?: ViewStyle
style?: ViewStyle
testID?: string
}
export const FormPhone = (props: IProps) => {
@ -56,13 +57,18 @@ export const FormPhone = (props: IProps) => { @@ -56,13 +57,18 @@ export const FormPhone = (props: IProps) => {
onChange={props.onChange}
styles={styles.input}
disabled={props.disabled}
testID={props.testID}
inputProps={{
placeholder: '+38 (068) 111 22 33',
maxLength: 19,
}}
/>
</View>
<Text style={styles.error}>{props.error}</Text>
<Text
style={styles.error}
testID={props.testID ? `${props.testID}Error` : null}>
{props.error}
</Text>
</View>
</>
)

4
src/shared/components/forms/form-select.component.tsx

@ -17,6 +17,7 @@ interface IProps { @@ -17,6 +17,7 @@ interface IProps {
buttonStyle?: ViewStyle
onPress?: () => void
disabled?: boolean
testID?: string
}
export const FormSelect: FC<IProps> = ({
@ -26,16 +27,19 @@ export const FormSelect: FC<IProps> = ({ @@ -26,16 +27,19 @@ export const FormSelect: FC<IProps> = ({
buttonStyle,
onPress,
disabled = false,
testID,
}) => {
const { styles, theme } = useTheme(createStyles)
return (
<>
<TouchableOpacity
testID={testID}
disabled={disabled}
onPress={onPress}
style={[styles.container, style]}>
<Txt
testID={testID ? `${testID}Value` : null}
style={[
styles.title,
disabled && { color: theme?.formSelect.$textDisabled },

11
src/shared/components/forms/form-text-input-with-icon.component.tsx

@ -26,6 +26,7 @@ interface IProps { @@ -26,6 +26,7 @@ interface IProps {
keybordType?: KeyboardTypeOptions
inputProps?: TextInputProps
error?: string
testID?: string
}
export const TextInputWithIcon: FC<IProps> = ({
@ -40,6 +41,7 @@ export const TextInputWithIcon: FC<IProps> = ({ @@ -40,6 +41,7 @@ export const TextInputWithIcon: FC<IProps> = ({
keybordType,
inputProps = {},
error,
testID,
}) => {
const isDefaultType = type === 'default'
const viewPointerEventType = isDefaultType ? 'auto' : 'none'
@ -60,6 +62,7 @@ export const TextInputWithIcon: FC<IProps> = ({ @@ -60,6 +62,7 @@ export const TextInputWithIcon: FC<IProps> = ({
pointerEvents={viewPointerEventType}
style={onCls(Boolean(error), 'inputWrap')}>
<TextInput
testID={testID}
editable={editable}
style={styles.input}
value={value}
@ -78,7 +81,13 @@ export const TextInputWithIcon: FC<IProps> = ({ @@ -78,7 +81,13 @@ export const TextInputWithIcon: FC<IProps> = ({
</View>
</Pressable>
{error && <Text style={styles.error}>{error}</Text>}
{error && (
<Text
testID={testID ? `${testID}Error` : null}
style={styles.error}>
{error}
</Text>
)}
</View>
)
}

7
src/shared/components/forms/form-textarea.component.tsx

@ -37,6 +37,7 @@ interface IProps { @@ -37,6 +37,7 @@ interface IProps {
disabled?: boolean
onSizeChange?: (reactNode: any) => void
inputProps?: TextInputProps
testID?: string
}
export const FormTextarea = ({ inputProps, ...props }: IProps) => {
@ -78,7 +79,11 @@ export const FormTextarea = ({ inputProps, ...props }: IProps) => { @@ -78,7 +79,11 @@ export const FormTextarea = ({ inputProps, ...props }: IProps) => {
/>
</View>
{props.error ? (
<Text style={styles.error}>{props.error}</Text>
<Text
style={styles.error}
testID={props.testID ? `${props.testID}Error` : null}>
{props.error}
</Text>
) : null}
{!props.disabled && Platform.OS === 'ios' ? (

14
src/shared/components/forms/touchable-fake-input.atom.tsx

@ -22,6 +22,7 @@ interface IProps { @@ -22,6 +22,7 @@ interface IProps {
txtStyle?: TextStyle
error?: string
disabled?: boolean
testID?: string
}
export const TouchableFakeInput: FC<IProps> = ({
@ -33,6 +34,7 @@ export const TouchableFakeInput: FC<IProps> = ({ @@ -33,6 +34,7 @@ export const TouchableFakeInput: FC<IProps> = ({
txtStyle,
disabled,
error,
testID,
}) => {
const { styles, theme, onCls } = useTheme(createStyles)
@ -44,7 +46,9 @@ export const TouchableFakeInput: FC<IProps> = ({ @@ -44,7 +46,9 @@ export const TouchableFakeInput: FC<IProps> = ({
disabled={disabled}
style={onCls(Boolean(error), 'inputWrap')}
onPress={onPress}>
<Text style={[styles.value, txtStyle]}>{value}</Text>
<Text testID={testID} style={[styles.value, txtStyle]}>
{value}
</Text>
<IconComponent
name={iconName}
@ -54,7 +58,13 @@ export const TouchableFakeInput: FC<IProps> = ({ @@ -54,7 +58,13 @@ export const TouchableFakeInput: FC<IProps> = ({
/>
</TouchableOpacity>
{error && <Text style={styles.error}>{error}</Text>}
{error && (
<Text
testID={testID ? `${testID}Error` : null}
style={styles.error}>
{error}
</Text>
)}
</View>
)
}

4
src/shared/components/images/img-with-bg-circle.components.tsx

@ -17,6 +17,7 @@ interface IProps { @@ -17,6 +17,7 @@ interface IProps {
style?: ViewStyle
height?: number
width?: number
testID?: string
}
export const ImgWithBgCircle: FC<IProps> = ({
@ -25,11 +26,12 @@ export const ImgWithBgCircle: FC<IProps> = ({ @@ -25,11 +26,12 @@ export const ImgWithBgCircle: FC<IProps> = ({
style,
height = 270,
width = 270,
testID,
}) => {
const { styles } = useTheme(createStyles)
return (
<View style={[style, styles.bgCircle]}>
<View style={[style, styles.bgCircle]} testID={testID}>
<ImageComponent
imageProps={{
source,

3
src/shared/components/layouts/screen-layout-scroll-content.componen.tsx

@ -16,6 +16,7 @@ interface ScreenLayoutContentProps { @@ -16,6 +16,7 @@ interface ScreenLayoutContentProps {
keyboardSpacerOn?: boolean
scrollRef?: React.MutableRefObject<KeyboardAwareScrollView>
extraHeight?: number
testID?: string
}
export const ScreenLayoutScrollContent: FC<ScreenLayoutContentProps> = ({
@ -26,6 +27,7 @@ export const ScreenLayoutScrollContent: FC<ScreenLayoutContentProps> = ({ @@ -26,6 +27,7 @@ export const ScreenLayoutScrollContent: FC<ScreenLayoutContentProps> = ({
scrollRef,
keyboardSpacerOn,
extraHeight = 160,
testID,
}) => {
const [keyboardScrollAware, setKeyboardScrollAware] = useState({
enable: true,
@ -43,6 +45,7 @@ export const ScreenLayoutScrollContent: FC<ScreenLayoutContentProps> = ({ @@ -43,6 +45,7 @@ export const ScreenLayoutScrollContent: FC<ScreenLayoutContentProps> = ({
<>
{header && header()}
<KeyboardAwareScrollView
testID={testID}
ref={scrollRef}
enableAutomaticScroll={keyboardScrollAware.enable}
keyboardShouldPersistTaps="always"

13
src/shared/components/layouts/screen-layout.component.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import React, { ReactElement, useCallback, useMemo } from 'react'
import React, { ReactElement, useCallback } from 'react'
import { StatusBar, StyleSheet, View, ViewStyle } from 'react-native'
import { ScreenLayoutContent } from './screen-layout-content.component'
import { PrimaryHeader } from '../headers'
@ -39,6 +39,7 @@ interface ScreenLayoutProps { @@ -39,6 +39,7 @@ interface ScreenLayoutProps {
onCloseDrawer: () => void
taskFilterProps: ITaskFilterProps
}
testID?: string
}
export const ScreenLayout = (props: ScreenLayoutProps) => {
@ -52,13 +53,19 @@ export const ScreenLayout = (props: ScreenLayoutProps) => { @@ -52,13 +53,19 @@ export const ScreenLayout = (props: ScreenLayoutProps) => {
const renderContent = () => {
if (props.needScroll) {
return <ScreenLayoutScrollContent {...props} header={header} />
return (
<ScreenLayoutScrollContent
{...props}
header={header}
testID={`${props.testID}Scroll`}
/>
)
}
return <ScreenLayoutContent {...props} header={header} />
}
const layout = (
<View style={styles.container} key="base">
<View style={styles.container} key="base" testID={props.testID}>
<StatusBar
barStyle={
themeTitle === 'light' ? 'dark-content' : 'light-content'

14
src/shared/components/modals/confirm-code-modal.component.tsx

@ -2,7 +2,6 @@ import { BottomModal, Button, RoundButton } from '@/shared/components' @@ -2,7 +2,6 @@ import { BottomModal, Button, RoundButton } from '@/shared/components'
import React, { useEffect, useRef, useState } from 'react'
import {
ActivityIndicator,
Dimensions,
StyleSheet,
Text,
TextInput,
@ -15,8 +14,7 @@ import { PartialTheme } from '@/shared/themes/interfaces' @@ -15,8 +14,7 @@ import { PartialTheme } from '@/shared/themes/interfaces'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import { useCountdown } from '@/shared/hooks'
import { Txt } from '../elements'
const screen = Dimensions.get('screen')
import { config } from '@/config'
interface IProps {
onSubmit(code: string): void
@ -28,7 +26,7 @@ interface IProps { @@ -28,7 +26,7 @@ interface IProps {
onClose?: () => void
}
const INITIAL_TIMOUT = 120
const INITIAL_TIMOUT = config.initialTimerCount || 120
export const ConfirmCodeModal = (props: IProps) => {
const { styles, theme } = useTheme(createStyles)
@ -95,7 +93,7 @@ export const ConfirmCodeModal = (props: IProps) => { @@ -95,7 +93,7 @@ export const ConfirmCodeModal = (props: IProps) => {
/>
)
return (
<Text style={styles.inputTitle}>
<Text style={styles.inputTitle} testID="timer">
На ваш телефон та email відправленний секретний код. Повторна
відправка можлива через{' '}
<Text style={styles.timer}>{getformattedTimer()}</Text>
@ -131,6 +129,7 @@ export const ConfirmCodeModal = (props: IProps) => { @@ -131,6 +129,7 @@ export const ConfirmCodeModal = (props: IProps) => {
/>
<TextInput
testID="confirmCodeInput"
maxLength={4}
ref={inputRef}
style={styles.input}
@ -142,8 +141,11 @@ export const ConfirmCodeModal = (props: IProps) => { @@ -142,8 +141,11 @@ export const ConfirmCodeModal = (props: IProps) => {
/>
</View>
<Txt style={styles.error}>{props.error}</Txt>
<Txt style={styles.error} testID="confirmCodeInputError">
{props.error}
</Txt>
<Button
testID="confirmCodeBtn"
title={'Підтвердити'}
onPress={() => props.onSubmit(code)}
type="primary"

3
src/shared/components/plugins/image-crop-picker.component.tsx

@ -11,6 +11,7 @@ interface IImageCropPickerProps { @@ -11,6 +11,7 @@ interface IImageCropPickerProps {
style?: ViewStyle
onChange: (image: unknown) => void
disabled?: boolean
testID?: string
}
export const ImageCropPicker: FC<IImageCropPickerProps> = props => {
@ -47,6 +48,7 @@ export const ImageCropPicker: FC<IImageCropPickerProps> = props => { @@ -47,6 +48,7 @@ export const ImageCropPicker: FC<IImageCropPickerProps> = props => {
height: avatarHeight,
cropperCircleOverlay: true,
})
onSuccessUpload(image)
}
@ -64,6 +66,7 @@ export const ImageCropPicker: FC<IImageCropPickerProps> = props => { @@ -64,6 +66,7 @@ export const ImageCropPicker: FC<IImageCropPickerProps> = props => {
return (
<TouchableOpacity
testID={props.testID}
disabled={props.disabled}
onPress={onPress}
style={props.style}>

2
src/shared/components/plugins/masked-input.component.tsx

@ -21,6 +21,7 @@ interface IProps { @@ -21,6 +21,7 @@ interface IProps {
styles?: any
inputProps?: any
disabled?: boolean
testID?: string
}
export const MaskedInput = (props: IProps) => {
@ -36,6 +37,7 @@ export const MaskedInput = (props: IProps) => { @@ -36,6 +37,7 @@ export const MaskedInput = (props: IProps) => {
return (
<TextInputMask
testID={props.testID}
type={props.type}
options={{
...getOptions(),

2
src/shared/consts/base64Avatar.js

File diff suppressed because one or more lines are too long

2
src/shared/consts/base64Background.js

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
export const base64BGString =
'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC'

28
src/shared/helpers/random-string.helper.js

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
const numbers = '0123456789'
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
export const makeRandomString = (length = 6, mod) => {
let result = ''
let characters
switch (mod) {
case 'num':
characters = numbers
break
case 'str':
characters = letters
break
default:
characters = letters + numbers
break
}
const charactersLength = characters.length
let counter = 0
while (counter < length) {
result += characters.charAt(
Math.floor(Math.random() * charactersLength),
)
counter += 1
}
return result
}
Loading…
Cancel
Save