diff --git a/.env.example b/.env.example index afc4dc09a6..dce068f091 100644 --- a/.env.example +++ b/.env.example @@ -1,69 +1,87 @@ -# [Optional] Phrase.com +# All environment variables listed here are not required for the application to start; +# however, missing values may impact application behavior in various ways. + +# Legend: +# [CI Mode] - Variable used only in Continuous Integration (CI) environments +# [Dev Mode] - Optional variable primarily used in the development environment +# [Setup Mode] - Variable required only during the setup and build process of CI Mode +# [Standard] - Common variable used across all environments + + +# [CI Mode] Mocks behavior of e2e tests run in CI. Default "false"; set "true" to enable. +E2ECI= + +# [CI Mode] Defines the path to external variables file +STATIC_CONFIGURATION_FILE_PATH= + +# [CI Mode] Set LOCALAPPDATA to empty for Windows platform to ensure clean environment in CI. +LOCALAPPDATA= + +# [Setup Mode] URL for mudita-dev-resources repo where gt-pressura font is placed +FONTS_DIRECTORY_URL= + +# [Setup Mode] Access token for dummy-account that has an access to Mudita private repositories +GITHUB_ACCESS_TOKEN= + +# [Standard] Phrase.com PHRASE_API_KEY= PHRASE_API_URL= -# [Optional, only in development mode] Phrase.com +# [Dev Mode] Phrase.com API key for development mode PHRASE_API_KEY_DEV= -# [Optional] Mudita Center Server URL to access to external services via proxy +# [Standard] Mudita Center Server URL to access to external services via proxy MUDITA_CENTER_SERVER_URL= MUDITA_CENTER_SERVER_V2_URL= -# [Optional] Rollbar Token needed to connect user’s app with Rollbar account. +# [Standard] Rollbar Token needed to connect user’s app with Rollbar account ROLLBAR_TOKEN= -# For Mudita developers only - -# [Optional] Access token for dummy-account that has an access to Mudita private repositories. This variable is needed only in development and build production app. -GITHUB_ACCESS_TOKEN= - -# [Optional] GitHub repository name for Mudita Center updates. Default: "mudita-center" +# [Standard] GitHub repository name for Mudita Center updates. Default: "mudita-center" RELEASES_REPOSITORY_NAME= -# [Optional] Enable pre release for update process. Disabledd by default, set "1" to enable +# [Dev Mode] Enable pre-release for update process. Disabled by default; set "1" to enable PRERELEASES_ENABLED= -# [Optional] Client id of the Microsoft Outlook application used for calendars and contacts +# [Standard] Client ID of the Microsoft Outlook application used for calendars and contacts LOGIN_MICROSOFT_ONLINE_CLIENT_ID= -# [Optional] URL for mudita-dev-resources repo where gt-pressura font is placed. -FONTS_DIRECTORY_URL= - -# [Optional] mudita.freshdesk.com +# [Standard] mudita.freshdesk.com FRESHDESK_API_URL= FRESHDESK_API_TOKEN= -# [Optional] analytics.mudita.com +# [Standard] analytics.mudita.com ANALYTICS_ENABLED= ANALYTICS_API_URL= ANALYTICS_API_SITE_ID= -# [Optional] Defines current feature toggles environment +# [Standard] Defines current feature toggles environment FEATURE_TOGGLE_ENVIRONMENT= -# [Optional] Defines the path to external variables file (used on CI) -STATIC_CONFIGURATION_FILE_PATH= - -# [Optional] Disable redux logger during development. Enabled by default, set "0" to disable +# [Dev Mode] Disable redux logger during development. Enabled by default; set "0" to disable DEV_REDUX_LOGGER_ENABLED= -# [Optional] Disable device logger during development. Enabled by default, set "0" to disable +# [Dev Mode] Disable device logger during development. Enabled by default; set "0" to disable DEV_DEVICE_LOGGER_ENABLED= -# [Optional] Set desired latest release. Is based on enum OsEnvironment, so can be set to 'production', 'test-production' or 'daily'. +# [Standard] Set desired latest release. Based on enum OsEnvironment; can be 'production', 'test-production' or 'daily' FEATURE_TOGGLE_RELEASE_ENVIRONMENT= -# [Optional] Enable Mudita Center prerelease feature. Disabled by default, set "1" to enable -MUDITA_CENTER_PRERELEASE_ENABLED= - -# [Optional] Set 1 run application with mock server +# [Dev Mode] Runs the application with a mock server. Disabled by default; set "1" to enable MOCK_SERVICE_ENABLED= -# [Optional] Enable shortcut for opening DevTools. Disabled by default, set "1" to enable +# [Dev Mode] Enable shortcut for opening DevTools and access to useDevConsole functionalities. +# Disabled by default; set "1" to enable DEV_TOOLS_SHORTCUT_ENABLED= -# [Optional] Automatically open DevTools on startup. Disabled by default, set "1" to enable +# [Dev Mode] Automatically open DevTools on startup. Disabled by default; set "1" to enable DEV_TOOLS_AUTO_OPEN_ENABLED= -# [Optional] Allows to show unpublished content in Help. Disabled by default, set a secret token to enable +# [Dev Mode] Allows showing unpublished content in Help. Disabled by default; set a secret token to enable DEV_HELP_PREVIEW_TOKEN= + +# [Dev Mode] Enable DEV version of API configuration. Disabled by default; set "1" to enable +DEV_API_CONFIG= + +# [Dev Mode] Path to directory with flash packages for Mudita Harmony +DEV_FLASH_PACKAGE_PATH= diff --git a/.github/workflows/e2e-development.yml b/.github/workflows/e2e-development.yml index fcbb2308ec..0d745babfc 100644 --- a/.github/workflows/e2e-development.yml +++ b/.github/workflows/e2e-development.yml @@ -41,7 +41,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} MOCK_SERVICE_ENABLED: ${{ secrets.MOCK_SERVICE_ENABLED }} run: | printenv > .env diff --git a/.github/workflows/e2e-feature-branch.yml b/.github/workflows/e2e-feature-branch.yml index 3961132b44..2622f5acca 100644 --- a/.github/workflows/e2e-feature-branch.yml +++ b/.github/workflows/e2e-feature-branch.yml @@ -1,4 +1,4 @@ -name: e2e on Linux for feature branch +name: e2e on Linux / Windows self-hosted for feature branch on: push: @@ -7,7 +7,10 @@ on: jobs: e2e: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ Windows, Linux ] environment: development steps: - name: Checkout code @@ -16,7 +19,8 @@ jobs: uses: actions/setup-node@v4 with: node-version: 18.16.1 - - name: Setup environment variables + - name: Setup environment variables for Linux + if: matrix.os == 'Linux' env: E2ECI: "true" TEST_GITHUB_TOKEN: ${{ secrets.MC_GITHUB_ACCESS_TOKEN }} @@ -42,18 +46,62 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} MOCK_SERVICE_ENABLED: ${{ secrets.MOCK_SERVICE_ENABLED }} run: | printenv > .env + - name: Setup environment variables for Windows + if: matrix.os == 'Windows' + env: + E2ECI: "true" + TEST_GITHUB_TOKEN: ${{ secrets.MC_GITHUB_ACCESS_TOKEN }} + TEST_BINARY_PATH: 'C:\actions-runner\_work\mudita-center\mudita-center\apps\mudita-center\release\win-unpacked\Mudita Center.exe' + PHRASE_API_KEY: ${{ secrets.PHRASE_API_KEY }} + PHRASE_API_URL: ${{ secrets.PHRASE_API_URL }} + PHRASE_API_KEY_DEV: ${{ secrets.PHRASE_API_KEY_DEV }} + MUDITA_CENTER_SERVER_URL: ${{ secrets.MUDITA_CENTER_SERVER_URL }} + MUDITA_CENTER_SERVER_V2_URL: ${{ secrets.MUDITA_CENTER_SERVER_V2_URL }} + ROLLBAR_TOKEN: ${{ secrets.ROLLBAR_TOKEN }} + RELEASES_REPOSITORY_NAME: ${{ secrets.RELEASES_REPOSITORY_NAME }} + PRERELEASES_ENABLED: ${{ secrets.PRERELEASES_ENABLED }} + GITHUB_ACCESS_TOKEN: ${{ secrets.MC_GITHUB_ACCESS_TOKEN }} + LOGIN_MICROSOFT_ONLINE_CLIENT_ID: ${{ secrets.LOGIN_MICROSOFT_ONLINE_CLIENT_ID }} + FONTS_DIRECTORY_URL: ${{ secrets.FONTS_DIRECTORY_URL }} + FRESHDESK_API_URL: ${{ secrets.FRESHDESK_API_URL }} + FRESHDESK_API_TOKEN: ${{ secrets.FRESHDESK_API_TOKEN }} + ANALYTICS_ENABLED: ${{ secrets.ANALYTICS_ENABLED }} + ANALYTICS_API_URL: ${{ secrets.ANALYTICS_API_URL }} + ANALYTICS_API_SITE_ID: ${{ secrets.ANALYTICS_API_SITE_ID }} + FEATURE_TOGGLE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_ENVIRONMENT }} + STATIC_CONFIGURATION_FILE_PATH: ${{ secrets.STATIC_CONFIGURATION_FILE_PATH }} + DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} + DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} + FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} + MOCK_SERVICE_ENABLED: ${{ secrets.MOCK_SERVICE_ENABLED }} + NEW_HELP_ENABLED: ${{ secrets.NEW_HELP_ENABLED }} + shell: cmd + run: | + SET > .env - name: Setup dependencies run: npm run setup - - name: Build App + - name: Build App for Linux + if: matrix.os == 'Linux' run: | export NODE_OPTIONS="--max-old-space-size=4096" npm run app:dist - - name: Run E2E tests headless with Xvfb + - name: Build App for Windows + if: matrix.os == 'Windows' + run: | + $env:NODE_OPTIONS="--max-old-space-size=4096" + $env:LOCALAPPDATA="" + npm run app:dist + - name: Run E2E tests headless with Xvfb on Linux + if: matrix.os == 'Linux' + run: | + npm run e2e:test:cicd:standalone + - name: Run E2E tests headless on Windows + env: + TEST_BINARY_PATH: 'C:\actions-runner\_work\mudita-center\mudita-center\apps\mudita-center\release\win-unpacked\Mudita Center.exe' + if: matrix.os == 'Windows' run: | - sudo apt-get update - sudo apt-get install -y xvfb + cd apps/mudita-center-e2e npm run e2e:test:cicd:standalone diff --git a/.github/workflows/e2e-pre-production.yml b/.github/workflows/e2e-pre-production.yml index 751c6b8648..f34c765d44 100644 --- a/.github/workflows/e2e-pre-production.yml +++ b/.github/workflows/e2e-pre-production.yml @@ -43,7 +43,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} MOCK_SERVICE_ENABLED: ${{ secrets.MOCK_SERVICE_ENABLED }} run: | printenv > .env diff --git a/.github/workflows/e2e-production.yml b/.github/workflows/e2e-production.yml index 317bb4e03c..61860cbb83 100644 --- a/.github/workflows/e2e-production.yml +++ b/.github/workflows/e2e-production.yml @@ -43,7 +43,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} MOCK_SERVICE_ENABLED: ${{ secrets.MOCK_SERVICE_ENABLED }} run: | printenv > .env diff --git a/.github/workflows/nexus-development.yml b/.github/workflows/nexus-development.yml index a2a089d3d3..c8b65e20ee 100644 --- a/.github/workflows/nexus-development.yml +++ b/.github/workflows/nexus-development.yml @@ -43,7 +43,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" LOCALAPPDATA: "" shell: cmd @@ -74,7 +73,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" run: | printenv > .env @@ -102,7 +100,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" run: | printenv > .env @@ -157,24 +154,6 @@ jobs: if: matrix.os == 'macOS' run: | codesign -v -v apps/mudita-center/release/mac/Mudita\ Center.app - - name: Signing via Digicert - if: matrix.os == 'Windows' - env: - SM_HOST: ${{ secrets.SM_HOST }} - SM_API_KEY: ${{ secrets.SM_API_KEY }} - SM_CLIENT_CERT_FILE: "C:\\actions-runner\\certs\\Certificate_pkcs12.p12" - SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} - SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} - run: | - SET SM_HOST=%SM_HOST% - SET SM_API_KEY=%SM_API_KEY% - SET SM_CLIENT_CERT_FILE=%SM_CLIENT_CERT_FILE% - SET SM_CLIENT_CERT_PASSWORD=%SM_CLIENT_CERT_PASSWORD% - SET SM_CODE_SIGNING_CERT_SHA1_HASH=%SM_CODE_SIGNING_CERT_SHA1_HASH% - SET PATH=%PATH%;"C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" - SET PATH=%PATH%;"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" - signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ./apps/mudita-center/release/Mudita-Center.exe - shell: cmd - name: Push artifacts to nexus registry from Windows if: matrix.os == 'Windows' env: diff --git a/.github/workflows/nexus-feature-branch.yml b/.github/workflows/nexus-feature-branch.yml index 05fe3307ab..594c746cf5 100644 --- a/.github/workflows/nexus-feature-branch.yml +++ b/.github/workflows/nexus-feature-branch.yml @@ -43,7 +43,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" LOCALAPPDATA: "" shell: cmd @@ -74,7 +73,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" run: | printenv > .env @@ -102,7 +100,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" run: | printenv > .env @@ -157,24 +154,6 @@ jobs: if: matrix.os == 'macOS' run: | codesign -v -v apps/mudita-center/release/mac/Mudita\ Center.app - - name: Signing via Digicert - if: matrix.os == 'Windows' - env: - SM_HOST: ${{ secrets.SM_HOST }} - SM_API_KEY: ${{ secrets.SM_API_KEY }} - SM_CLIENT_CERT_FILE: "C:\\actions-runner\\certs\\Certificate_pkcs12.p12" - SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} - SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} - run: | - SET SM_HOST=%SM_HOST% - SET SM_API_KEY=%SM_API_KEY% - SET SM_CLIENT_CERT_FILE=%SM_CLIENT_CERT_FILE% - SET SM_CLIENT_CERT_PASSWORD=%SM_CLIENT_CERT_PASSWORD% - SET SM_CODE_SIGNING_CERT_SHA1_HASH=%SM_CODE_SIGNING_CERT_SHA1_HASH% - SET PATH=%PATH%;"C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" - SET PATH=%PATH%;"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" - signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ./apps/mudita-center/release/Mudita-Center.exe - shell: cmd - name: Push artifacts to nexus registry from Windows if: matrix.os == 'Windows' env: diff --git a/.github/workflows/nexus-mass-update.yml b/.github/workflows/nexus-mass-update.yml index 955b5abfdc..cabf05a579 100644 --- a/.github/workflows/nexus-mass-update.yml +++ b/.github/workflows/nexus-mass-update.yml @@ -45,7 +45,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} LOCALAPPDATA: "" shell: cmd run: | @@ -75,7 +74,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} run: | printenv > .env - name: Setup Env for Linux @@ -102,7 +100,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} run: | printenv > .env - name: Changing app version in packages.json for Linux diff --git a/.github/workflows/nexus-mock-development.yml b/.github/workflows/nexus-mock-development.yml index 12a1f29e2a..fc891dc468 100644 --- a/.github/workflows/nexus-mock-development.yml +++ b/.github/workflows/nexus-mock-development.yml @@ -43,7 +43,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" LOCALAPPDATA: "" MOCK_SERVICE_ENABLED: "1" @@ -75,7 +74,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" MOCK_SERVICE_ENABLED: "1" run: | @@ -104,7 +102,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} MOCK_SERVICE_ENABLED: "1" DEV_TOOLS_SHORTCUT_ENABLED: "1" run: | @@ -160,24 +157,6 @@ jobs: if: matrix.os == 'macOS' run: | codesign -v -v apps/mudita-center/release/mac/Mudita\ Center.app - - name: Signing via Digicert - if: matrix.os == 'Windows' - env: - SM_HOST: ${{ secrets.SM_HOST }} - SM_API_KEY: ${{ secrets.SM_API_KEY }} - SM_CLIENT_CERT_FILE: "C:\\actions-runner\\certs\\Certificate_pkcs12.p12" - SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} - SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} - run: | - SET SM_HOST=%SM_HOST% - SET SM_API_KEY=%SM_API_KEY% - SET SM_CLIENT_CERT_FILE=%SM_CLIENT_CERT_FILE% - SET SM_CLIENT_CERT_PASSWORD=%SM_CLIENT_CERT_PASSWORD% - SET SM_CODE_SIGNING_CERT_SHA1_HASH=%SM_CODE_SIGNING_CERT_SHA1_HASH% - SET PATH=%PATH%;"C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" - SET PATH=%PATH%;"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" - signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ./apps/mudita-center/release/Mudita-Center.exe - shell: cmd - name: Push artifacts to nexus registry from Windows if: matrix.os == 'Windows' env: diff --git a/.github/workflows/nexus-mock-pre-production.yml b/.github/workflows/nexus-mock-pre-production.yml index 6c357357fc..ad46d3d2e6 100644 --- a/.github/workflows/nexus-mock-pre-production.yml +++ b/.github/workflows/nexus-mock-pre-production.yml @@ -45,7 +45,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" LOCALAPPDATA: "" MOCK_SERVICE_ENABLED: "1" @@ -77,7 +76,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" MOCK_SERVICE_ENABLED: "1" run: | diff --git a/.github/workflows/nexus-mock-production.yml b/.github/workflows/nexus-mock-production.yml index a779823660..8a5817b55f 100644 --- a/.github/workflows/nexus-mock-production.yml +++ b/.github/workflows/nexus-mock-production.yml @@ -45,7 +45,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" LOCALAPPDATA: "" MOCK_SERVICE_ENABLED: "1" @@ -77,7 +76,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} DEV_TOOLS_SHORTCUT_ENABLED: "1" MOCK_SERVICE_ENABLED: "1" run: | @@ -127,24 +125,6 @@ jobs: if: matrix.os == 'macOS' run: | codesign -v -v apps/mudita-center/release/mac/Mudita\ Center.app - - name: Signing via Digicert - if: matrix.os == 'Windows' - env: - SM_HOST: ${{ secrets.SM_HOST }} - SM_API_KEY: ${{ secrets.SM_API_KEY }} - SM_CLIENT_CERT_FILE: "C:\\actions-runner\\certs\\Certificate_pkcs12.p12" - SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} - SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} - run: | - SET SM_HOST=%SM_HOST% - SET SM_API_KEY=%SM_API_KEY% - SET SM_CLIENT_CERT_FILE=%SM_CLIENT_CERT_FILE% - SET SM_CLIENT_CERT_PASSWORD=%SM_CLIENT_CERT_PASSWORD% - SET SM_CODE_SIGNING_CERT_SHA1_HASH=%SM_CODE_SIGNING_CERT_SHA1_HASH% - SET PATH=%PATH%;"C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" - SET PATH=%PATH%;"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" - signtool.exe sign /sha1 %SM_CODE_SIGNING_CERT_SHA1_HASH% /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ./apps/mudita-center/release/Mudita-Center.exe - shell: cmd - name: Push artifacts to nexus registry from Windows if: matrix.os == 'Windows' env: diff --git a/.github/workflows/nexus-pre-production-latest.yml b/.github/workflows/nexus-pre-production-latest.yml index a1dbfe02aa..88cd85d837 100644 --- a/.github/workflows/nexus-pre-production-latest.yml +++ b/.github/workflows/nexus-pre-production-latest.yml @@ -46,7 +46,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} LOCALAPPDATA: "" shell: cmd run: | @@ -76,7 +75,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} run: | printenv > .env - name: Setup Env for Linux @@ -103,7 +101,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} run: | printenv > .env - name: Changing app version in packages.json for Linux & Standard Update diff --git a/.github/workflows/nexus-pre-production.yml b/.github/workflows/nexus-pre-production.yml index 1373c40d7f..b8ab37b59c 100644 --- a/.github/workflows/nexus-pre-production.yml +++ b/.github/workflows/nexus-pre-production.yml @@ -45,7 +45,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} LOCALAPPDATA: "" shell: cmd run: | @@ -75,7 +74,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} run: | printenv > .env - name: Setup depedencies diff --git a/.github/workflows/nexus-production-with-os-rc.yml b/.github/workflows/nexus-production-with-os-rc.yml index 8742ecad96..16881e260b 100644 --- a/.github/workflows/nexus-production-with-os-rc.yml +++ b/.github/workflows/nexus-production-with-os-rc.yml @@ -45,7 +45,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} LOCALAPPDATA: "" shell: cmd run: | @@ -75,7 +74,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} run: | printenv > .env - name: Setup Env for Linux @@ -102,7 +100,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} run: | printenv > .env - name: Changing app version in packages.json for Linux diff --git a/.github/workflows/nexus-production.yml b/.github/workflows/nexus-production.yml index ed3e0b0423..ddf5858b22 100644 --- a/.github/workflows/nexus-production.yml +++ b/.github/workflows/nexus-production.yml @@ -45,7 +45,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} LOCALAPPDATA: "" shell: cmd run: | @@ -75,7 +74,6 @@ jobs: DEV_REDUX_LOGGER_ENABLED: ${{ secrets.DEV_REDUX_LOGGER_ENABLED }} DEV_DEVICE_LOGGER_ENABLED: ${{ secrets.DEV_DEVICE_LOGGER_ENABLED }} FEATURE_TOGGLE_RELEASE_ENVIRONMENT: ${{ secrets.FEATURE_TOGGLE_RELEASE_ENVIRONMENT }} - MUDITA_CENTER_PRERELEASE_ENABLED: ${{ secrets.MUDITA_CENTER_PRERELEASE_ENABLED }} run: | printenv > .env - name: Setup depedencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f7b93eec..5eac8e5572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Here we write upgrading notes for brands. It's a team effort to make them as straightforward as possible. +## [2.5.0] - 2024-12-02 + +Starting with version 2.5.0, users will be able to synchronize the time and date on the Harmony and will be informed about an error in case of fail in the synchronization. + +### Added + +- Added time synchronization for Mudita Harmony. + ## [2.4.0] - 2024-09-16 In this release, we have updated the Help section in the Mudita Center application. As of version 2.4.0, users can benefit from a refreshed help section with intuitive navigation. The article search functionality is also available. In addition, we have fixed two bugs occurring in our software. diff --git a/apps/mudita-center-e2e/src/helpers/index.ts b/apps/mudita-center-e2e/src/helpers/index.ts index 844f85e401..62a4e2bea6 100644 --- a/apps/mudita-center-e2e/src/helpers/index.ts +++ b/apps/mudita-center-e2e/src/helpers/index.ts @@ -4,3 +4,4 @@ */ export * from "./sleep.helper" +export * from "./testid.helper" diff --git a/apps/mudita-center-e2e/src/helpers/testid.helper.ts b/apps/mudita-center-e2e/src/helpers/testid.helper.ts new file mode 100644 index 0000000000..be1037dee1 --- /dev/null +++ b/apps/mudita-center-e2e/src/helpers/testid.helper.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +const PRIMARY_BUTTON_TEST_ID = "primary-button" + +export const getPrimaryButtonTestId = (componentId: string) => { + return `${PRIMARY_BUTTON_TEST_ID}-${componentId}` +} diff --git a/apps/mudita-center-e2e/src/page-objects/contacts.page.ts b/apps/mudita-center-e2e/src/page-objects/contacts.page.ts index a92ed4d1c5..80b5027262 100644 --- a/apps/mudita-center-e2e/src/page-objects/contacts.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/contacts.page.ts @@ -3,14 +3,11 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class ContactsPage extends Page { /**[Selector] New contact button on Contacts screen */ - public get newContactButton(): ChainablePromiseElement< - Promise - > { + public get newContactButton() { return $('[data-testid="new-contact-button"]') } @@ -21,23 +18,17 @@ class ContactsPage extends Page { } /**[Selector] Import button on Contacts screen*/ - public get importButton(): ChainablePromiseElement< - Promise - > { + public get importButton() { return $('[data-testid="import-button"]') } /**[Selector] Search contacts input*/ - public get searchContactsInput(): ChainablePromiseElement< - Promise - > { + public get searchContactsInput() { return $('[data-testid="contact-input-select-input"]') } /**[Selector] First name input*/ - public get firstNameInput(): ChainablePromiseElement< - Promise - > { + public get firstNameInput() { return $('[data-testid="first-name"]') } /**Insert provided text to First Name input field*/ @@ -47,9 +38,7 @@ class ContactsPage extends Page { } /**[Selector] Last name input*/ - public get lastNameInput(): ChainablePromiseElement< - Promise - > { + public get lastNameInput() { return $('[data-testid="second-name"]') } @@ -60,9 +49,7 @@ class ContactsPage extends Page { } /**[Selector] Primary phone number input*/ - public get primaryPhoneNumberInput(): ChainablePromiseElement< - Promise - > { + public get primaryPhoneNumberInput() { return $('[data-testid="primary-number"]') } @@ -73,9 +60,7 @@ class ContactsPage extends Page { } /**[Selector] Secondary phone number input*/ - public get secondaryPhoneNumberInput(): ChainablePromiseElement< - Promise - > { + public get secondaryPhoneNumberInput() { return $('[data-testid="secondary-number"]') } @@ -85,9 +70,7 @@ class ContactsPage extends Page { await this.secondaryPhoneNumberInput.setValue(textValue) } /**[Selector] Adress first line input*/ - public get addressFirstLineInput(): ChainablePromiseElement< - Promise - > { + public get addressFirstLineInput() { return $('[data-testid="first-address-line"]') } @@ -98,9 +81,7 @@ class ContactsPage extends Page { } /**[Selector] Adress second line input*/ - public get addressSecondLineInput(): ChainablePromiseElement< - Promise - > { + public get addressSecondLineInput() { return $('[data-testid="second-address-line"]') } @@ -111,9 +92,7 @@ class ContactsPage extends Page { } /**[Selector] Close button on contact detail screen*/ - public get closeButton(): ChainablePromiseElement< - Promise - > { + public get closeButton() { return $('[data-testid="icon-Close"]') } @@ -123,22 +102,16 @@ class ContactsPage extends Page { await this.closeButton.click() } /**[Selector] add to favourites checkbox*/ - public get addToFavouritessCheckbox(): ChainablePromiseElement< - Promise - > { + public get addToFavouritessCheckbox() { return $('[name*="favourite"]') } /**[Selector] Cancel button on add/edit contact screen*/ - public get cancelButton(): ChainablePromiseElement< - Promise - > { + public get cancelButton() { return $("p*=Cancel") } /** [Selector] Save contact button*/ - public get saveContactButton(): ChainablePromiseElement< - Promise - > { + public get saveContactButton() { return $('[data-testid="save-button"]') } /** Click on Save contact button */ @@ -147,9 +120,7 @@ class ContactsPage extends Page { await this.saveContactButton.click() } - public get noContactsTextLabel(): ChainablePromiseElement< - Promise - > { + public get noContactsTextLabel() { return $('[data-testid="contact-list-no-result"]') } @@ -157,21 +128,15 @@ class ContactsPage extends Page { return $$('[data-testid="virtualized-contact-list-item-contact-row"]') } - public get singleContactRow(): ChainablePromiseElement< - Promise - > { + public get singleContactRow() { return $('[data-testid="virtualized-contact-list-item-contact-row"]') } - public get phoneNumberOnContactList(): ChainablePromiseElement< - Promise - > { + public get phoneNumberOnContactList() { return $('[data-testid="virtualized-contact-phone-number"]') } - public get optionsButtonOnContactList(): ChainablePromiseElement< - Promise - > { + public get optionsButtonOnContactList() { return $('[data-testid="icon-More"]') } async optionsButtonOnContactListClick() { @@ -179,69 +144,47 @@ class ContactsPage extends Page { await this.optionsButtonOnContactList.click() } - public get editContactOptionMenu(): ChainablePromiseElement< - Promise - > { + public get editContactOptionMenu() { return $('[data-testid="icon-Edit"]') } - public get exportAsVCardOptionMenu(): ChainablePromiseElement< - Promise - > { + public get exportAsVCardOptionMenu() { return $('[data-testid="icon-UploadDark"]') } - public get deleteContactOptionMenu(): ChainablePromiseElement< - Promise - > { + public get deleteContactOptionMenu() { return $('[data-testid="icon-Delete"]') } - public get nameOnContactDetailScreen(): ChainablePromiseElement< - Promise - > { + public get nameOnContactDetailScreen() { return $('[data-testid="name"]') } - public get phoneNumber1OnContactDetailScreen(): ChainablePromiseElement< - Promise - > { + public get phoneNumber1OnContactDetailScreen() { return $('[data-testid="primary-phone-input"]') } - public get phoneNumber2OnContactDetailScreen(): ChainablePromiseElement< - Promise - > { + public get phoneNumber2OnContactDetailScreen() { return $('[data-testid="secondary-phone-input"]') } - public get addressOnContactDetailScreen(): ChainablePromiseElement< - Promise - > { + public get addressOnContactDetailScreen() { return $('[data-testid="address-details"]') } - public get buttonDeleteOnContactDetailScreen(): ChainablePromiseElement< - Promise - > { + public get buttonDeleteOnContactDetailScreen() { return $('[data-testid="icon-Delete"]') } - public get buttonExportOnContactDetailScreen(): ChainablePromiseElement< - Promise - > { + public get buttonExportOnContactDetailScreen() { return $('[data-testid="icon-UploadDark"]') } - public get editButtonOnContactDetailScreen(): ChainablePromiseElement< - Promise - > { + public get editButtonOnContactDetailScreen() { return $('[data-testid="icon-Edit"]') } - public get checkboxSingleContact(): ChainablePromiseElement< - Promise - > { + public get checkboxSingleContact() { return $('[type="checkbox"]') } @@ -249,57 +192,39 @@ class ContactsPage extends Page { return $$('[type="checkbox"]') } - public get checkboxSelectAll(): ChainablePromiseElement< - Promise - > { + public get checkboxSelectAll() { return $('[data-testid="selection-manager"]').$('[type="checkbox"]') } - public get textSummaryOfContactsSelected(): ChainablePromiseElement< - Promise - > { + public get textSummaryOfContactsSelected() { return $('[data-testid="info"]') } - public get buttonDeleteSelectionManager(): ChainablePromiseElement< - Promise - > { + public get buttonDeleteSelectionManager() { return $("p*=Delete") } - public get inputHiddenVcfFile(): ChainablePromiseElement< - Promise - > { + public get inputHiddenVcfFile() { return $('[data-testid="file-input"]') } - public get buttonContinueWithGoogle(): ChainablePromiseElement< - Promise - > { + public get buttonContinueWithGoogle() { return $('[data-testid="google-button"]') } - public get buttonOutlookImport(): ChainablePromiseElement< - Promise - > { + public get buttonOutlookImport() { return $('[data-testid="outlook-button"]') } - public get buttonImportFromVCFFileImport(): ChainablePromiseElement< - Promise - > { + public get buttonImportFromVCFFileImport() { return $('[data-testid="icon-Upload"]') } - public get inputSearch(): ChainablePromiseElement< - Promise - > { + public get inputSearch() { return $('[data-testid="contact-input-select-input"]') } - public get dropDownSearchResultList(): ChainablePromiseElement< - Promise - > { + public get dropDownSearchResultList() { return $('[data-testid="input-select-list"]') } @@ -310,9 +235,7 @@ class ContactsPage extends Page { ) } - public get contactDetailsFavouritesIcon(): ChainablePromiseElement< - Promise - > { + public get contactDetailsFavouritesIcon() { return $('[data-testid="icon-Favourites"]') } } diff --git a/apps/mudita-center-e2e/src/page-objects/help-article.page.ts b/apps/mudita-center-e2e/src/page-objects/help-article.page.ts new file mode 100644 index 0000000000..a0521c0f66 --- /dev/null +++ b/apps/mudita-center-e2e/src/page-objects/help-article.page.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ChainablePromiseElement } from "webdriverio" +import Page from "./page" + +class HelpArticlePage extends Page { + public get helpTabTitle() { + return $('[data-testid="location"]') + } + public get helpArticleBackButton() { + return $('[data-testid="help-article-back-button"]') + } + public get helpArticleItems() { + return $$('[data-testid="help-subcategory-articles-list-item"]') + } + public get helpArticleTitle() { + return $('[data-testid="help-article-title"]') + } + public get helpArticleWarningIcon() { + return $('[data-testid="help-article-warning-icon"]') + } + public get helpArticleWarning() { + return $('[data-testid="help-article-warning"]') + } + public get helpArticleContent() { + return $('[data-testid="help-article-content"]') + } + public get helpArticleContentBlocks() { + return $$('[data-testid="help-article-content-block"]') + } + getHelpArticleContentBlock(index: number) { + return $$('[data-testid="help-article-content-block"]')[index] + } + getHelpArticleContentBlockTitle(index: number) { + return $$('[data-testid="help-article-content-block-title"]')[index] + } + public get helpArticleContentBlockText() { + return $$('[data-testid="help-article-content-block-text"]') + } + getHelpArticleContentBlockText(index: number) { + return $$('[data-testid="help-article-content-block"]')[index].$( + '[data-testid="help-article-content-block-text"]' + ) + } + public get helpArticleFeedback() { + return $('[data-testid="help-article-feedback"]') + } + public get helpArticleFeedbackTitle() { + return $('[data-testid="help-article-feedback-title"]') + } + public get helpArticleFeedbackYesButton() { + return $('[data-testid="help-article-feedback-yes-button"]') + } + public get helpArticleFeedbackNoButton() { + return $('[data-testid="help-article-feedback-no-button"]') + } + public get iconNamaste() { + return $('[data-testid="icon-namaste"]') + } + public get feedbackThanksText() { + return $('[data-testid="help-article-feedback-thanks"]') + } + public get helpArticleFooter() { + return $('[data-testid="help-article-footer"]') + } + public get helpArticleFooterTitle() { + return $('[data-testid="help-article-footer-title"]') + } + public get helpArticleFooterButton() { + return $('[data-testid="help-article-footer-button"]') + } + public get helpArticleFooterVisitSupportButton() { + return $('[data-testid="help-article-footer-button"]') + } + public get helpCategories() { + return $$('[data-testid="help-categories-list-item"]') + } + public get connectYourDeviceLinks() { + return $$('[data-testid="help-article-content-block-internal-link"]') + } +} + +export default new HelpArticlePage() diff --git a/apps/mudita-center-e2e/src/page-objects/help-modal.page.ts b/apps/mudita-center-e2e/src/page-objects/help-modal.page.ts index ff1d6d1529..acc273595f 100644 --- a/apps/mudita-center-e2e/src/page-objects/help-modal.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/help-modal.page.ts @@ -3,68 +3,95 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class HelpModalPage extends Page { /**[Selector] Modal close button */ - public get closeModalButton(): ChainablePromiseElement< - Promise - > { + public get closeModalButton() { return $('[data-testid="close-modal-button"]') } /**[Selector] Modal title */ - public get modalHeader(): ChainablePromiseElement< - Promise - > { + public get modalHeader() { return $('[data-testid="modal-header"]') } /**[Selector] Contact support email input */ - public get emailInput(): ChainablePromiseElement< - Promise - > { + public get emailInput() { return $('[data-testid="email-input"]') } /**[Selector] Contact support message input */ - public get descriptionInput(): ChainablePromiseElement< - Promise - > { + public get descriptionInput() { return $('[data-testid="description-input"]') } /**[Selector] Send button */ - public get sendButton(): ChainablePromiseElement< - Promise - > { + public get sendButton() { return $('[data-testid="submit-button"]') } - + /**[Selector] Send button label */ + public get sendButtonLabel() { + return $('//button[@data-testid="submit-button"]//p') + } /** returns an Array containing list of attached files */ async attachmentsList() { return $('[data-testid="file-list"]').$$('[data-testid="file-list-file"]') } /**[Selector] single attachment element */ - public get singleAttachment(): ChainablePromiseElement< - Promise - > { + public get singleAttachment() { return $('[data-testid="file-list-file"]') } /**[Selector] Success sent message modal */ - public get sentSuccessModal(): ChainablePromiseElement< - Promise - > { + public get sentSuccessModal() { return $('[data-testid="contact-support-modal-success"]') } /**[Selector] Close bottom button */ - public get closeBottomButton(): ChainablePromiseElement< - Promise - > { + public get closeBottomButton() { return $('[data-testid="close-bottom-button"]') } - public get invalidEmailTextElement(): ChainablePromiseElement< - Promise - > { + /**[Selector] Invalid email text */ + public get invalidEmailTextElement() { return $('//input[@data-testid="email-input"]/following-sibling::*[1]') } + + /**[Selector] Support icon */ + public get iconSupport() { + return $('[data-testid="icon-Support"]') + } + /**[Selector] Attachment icon */ + public get iconAttachment() { + return $('[data-testid="icon-Attachment"]') + } + /**[Selector] Modal title with specific text */ + public get modalTitle() { + return $('[data-testid="contact-support-modal-title"]') + } + /**[Selector] Modal subtitle */ + public get modalSubtitle() { + return $('[data-testid="contact-support-modal-subtitle"]') + } + /**[Selector] Current date zip file */ + public get currentDateZipFile() { + const currentDate = new Date().toISOString().split("T")[0] // Get current date in YYYY-MM-DD format + return $(`p*=${currentDate}.zip`) + } + /**[Selector] Attached files text */ + public get attachedFilesLabel() { + return $('[data-testid="attached-files-label"]') + } + /**[Selector] Attached files subtext */ + public get attachedFilesSubText() { + return $('[data-testid="attached-files-subtext"]') + } + /**[Selector] Email label */ + public get emailLabel() { + return $('[data-testid="email-label"]') + } + /**[Selector] Message label */ + public get messageLabel() { + return $('[data-testid="message-label"]') + } + /**[Selector] Whole modal */ + public get wholeModal() { + return $('[data-testid="contact-support-modal"]') + } } export default new HelpModalPage() diff --git a/apps/mudita-center-e2e/src/page-objects/help.page.ts b/apps/mudita-center-e2e/src/page-objects/help.page.ts index 38b7fdaf70..419eca5fbf 100644 --- a/apps/mudita-center-e2e/src/page-objects/help.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/help.page.ts @@ -3,44 +3,74 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class HelpPage extends Page { - public get listElement() { - return $('[data-testid="help-component-question"]') - } - public get listElements() { - return $$('[data-testid="help-component-question"]') + //Getters + public get helpTabTitle() { + return $('//h4[@data-testid="location"]') } - - public get windowTitle() { - return $('[data-testid="help-component-title"]') + public get helpMainHeader() { + return $('//h3[@data-testid="help-main-header"]') } - - public get searchIcon() { - return $('[data-testid="icon-Magnifier"]') + public get helpMainSubHeader() { + return $('//p[@data-testid="help-main-subheader"]') } - - public get searchPlaceholder() { - return $('[type="search"]') + public get iconSearch() { + return $('//div[@data-testid="icon-search"]') } - - public get topicContent() { - return $('[data-testid="content"]') + public get helpSearchInput() { + return $('//input[@data-testid="help-search-input"]') } - - public get articleBackLink() { - return $('[data-testid="back-link"]') + public get helpSearchResults() { + return $('//div[@data-testid="help-search-results"]') } - - public get contactSupportButton() { - return $('[data-testid="help-support-button"]') + public get iconSearchHelpSearchResults() { + return $('//div[@data-testid="help-search-results"]//div[@data-testid="icon-search"]') } - - public get contactSupportButtonTooltip() { - return $('[data-testid="icon-button-with-tooltip-description"]') + public get helpSearchResultsParagraph() { + return $('//div[@data-testid="help-search-results"]//p') + } + public get helpSearchResultsList() { + return $('//div[@data-testid="help-search-results"]//ul') + } + public get helpSearchResultsItems() { + return $$('//a[@data-testid="help-search-result-item"]') + } + public get helpCategoriesTitle() { + return $('//h2[@data-testid="help-categories-title"]') + } + public get helpCategoriesList() { + return $('//nav[@data-testid="help-categories-list"]') + } + public get helpCategoriesListItems() { + return $$('//a[@data-testid="help-categories-list-item"]') + } + public get helpSubCategoriesList() { + return $('//div[@data-testid="help-subcategories-list"]') + } + public get helpSubCategoriesListItems() { + return $$('//div[@data-testid="help-subcategories-list-item"]') + } + public getHelpSubCategoriesListItemsFromColumn(columnIndex: number){ + return $$(`(//div[@data-testid="help-subcategories-list"]/div)[${columnIndex + 1}]//div[@data-testid="help-subcategories-list-item"]`) + } + public get helpSubCategoriesListItemsLeftColumn() { + return this.getHelpSubCategoriesListItemsFromColumn(0) + } + public get helpSubCategoriesListItemsRightColumn() { + return this.getHelpSubCategoriesListItemsFromColumn(1) + } + public get helpMainFooterDescription() { + return $('//p[@data-testid="help-main-footer-description"]') + } + public get helpMainFooterContactSupportButton() { + return $('//button[@data-testid="help-main-footer-contact-support-button"]') + } + public async searchForArticle(text:string) { + const searchInput = await this.helpSearchInput + await searchInput.setValue(text) } } diff --git a/apps/mudita-center-e2e/src/page-objects/home.page.ts b/apps/mudita-center-e2e/src/page-objects/home.page.ts index 305bbb59f6..4246eb35db 100644 --- a/apps/mudita-center-e2e/src/page-objects/home.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/home.page.ts @@ -7,73 +7,79 @@ import Page from "./page" class HomePage extends Page { get homeHeader() { - return $("h1[data-testid='onboarding-title-header']") + return $("//h1[@data-testid='onboarding-title-header']") } get notNowButton() { - return $("button[data-testid='onboarding-not-now-button']") + return $("//button[@data-testid='onboarding-not-now-button']") } get myDevicesDoesntShowButton() { return $( - "button[data-testid='onboarding-my-device-does-not-show-up-button']" + "//button[@data-testid='onboarding-my-device-does-not-show-up-button']" ) } get weAreSorryPageHeader() { - return $("h2[data-testid=onboarding-troubleshooting-title-header]") + return $("//h2[@data-testid='onboarding-troubleshooting-title-header']") } get weAreSorryPageFollowTheInstructionsParagraph() { - return $("p[data-testid='onboarding-troubleshooting-subtitle-paragraph']") + return $("//p[@data-testid='onboarding-troubleshooting-subtitle-paragraph']") } get weAreSorryPageInstructionsList() { return $$( - "ol li[data-testid='onboarding-troubleshooting-instruction-step-list-item']" + "//ol/li[@data-testid='onboarding-troubleshooting-instruction-step-list-item']" ) } get thisDidntSolveParagraph() { return $( - "button[data-testid='onboarding-troubleshooting-ui-more-instructions'] p" + "//button[@data-testid='onboarding-troubleshooting-ui-more-instructions']/p" ) } get tryAgainParagraph() { - return $("button[data-testid='onboarding-troubleshooting-ui-retry'] p") + return $("//button[@data-testid='onboarding-troubleshooting-ui-retry']/p") } get contactSupportButton() { - return $("[data-testid='onboarding-troubleshooting-ui-contact-support']") + return $("//button[@data-testid='onboarding-troubleshooting-ui-contact-support']") } get muditaCenterSupportModalHeader() { - return $("h2[data-testid='modal-title']") + return $("//h2[@data-testid='modal-title']") } get emailField() { - return $("input[data-testid='email-input']") + return $("//input[@data-testid='email-input']") } get messageField() { - return $("textarea[data-testid='description-input']") + return $("//textarea[@data-testid='description-input']") } get attachedFile() { - return $("ul[data-testid='file-list']") + return $("//ul[@data-testid='file-list']") } get sendButton() { - return $("button[data-testid='submit-button']") + return $("//button[@data-testid='submit-button']") } get closeCenterSupportModalButton() { - return $("button[data-testid='close-modal-button']") + return $("//button[@data-testid='close-modal-button']") } get centerSupportModal() { - return $("div[data-testid='contact-support-modal']") + return $("//div[@data-testid='contact-support-modal']") + } + + public async clickNotNowButton() { + const button = await this.notNowButton; + await button.waitForDisplayed(); + await button.click(); } } export default new HomePage() diff --git a/apps/mudita-center-e2e/src/page-objects/messages-browse-contacts-modal.page.ts b/apps/mudita-center-e2e/src/page-objects/messages-browse-contacts-modal.page.ts index 831ca34c8d..ec28d22c17 100644 --- a/apps/mudita-center-e2e/src/page-objects/messages-browse-contacts-modal.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/messages-browse-contacts-modal.page.ts @@ -3,39 +3,28 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import ModalPage from "./modal.page" class BrowseContactsModal extends ModalPage { - public get emptyContactList(): ChainablePromiseElement< - Promise - > { + public get emptyContactList() { return $('[data-testid="empty-content"]') } - public get searchInput(): ChainablePromiseElement< - Promise - > { + public get searchInput() { return $('[data-testid="contact-input-select-input"]') } - public get modalContactNameText(): ChainablePromiseElement< - Promise - > { + public get modalContactNameText() { return $( '[data-testid="contact-simple-list-phone-selection-item-avatar-column"]' ) } - public get modalContactPrimaryNumberText(): ChainablePromiseElement< - Promise - > { + public get modalContactPrimaryNumberText() { return $( '[data-testid="contact-simple-list-phone-selection-item-primary-phone-field"]' ) } - public get modalContactSecondaryNumberText(): ChainablePromiseElement< - Promise - > { + public get modalContactSecondaryNumberText() { return $( '[data-testid="contact-simple-list-phone-selection-item-secondary-phone-field"]' ) diff --git a/apps/mudita-center-e2e/src/page-objects/messages-conversation.page.ts b/apps/mudita-center-e2e/src/page-objects/messages-conversation.page.ts index 50bb470e9c..ca9852fdf3 100644 --- a/apps/mudita-center-e2e/src/page-objects/messages-conversation.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/messages-conversation.page.ts @@ -3,90 +3,65 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class MessagesConversationPage extends Page { /** [Selector] NEW MESSAGE button selector */ - public get newMessageButton(): ChainablePromiseElement< - Promise - > { + public get newMessageButton() { return $('[data-testid="message-panel-new-message-button"]') } /** [Selector] Search Contacts input field (visible after NEW MESSAGE button is clicked) */ - public get searchContactsInput(): ChainablePromiseElement< - Promise - > { + public get searchContactsInput() { return $('[data-testid="receiver-input-select-input"]') } /** [Selector] Input field for message text */ - public get messageTextInput(): ChainablePromiseElement< - Promise - > { + public get messageTextInput() { return $('[data-testid="thread-details-text-area-input"]') } /** [Selector] Send message button (visible after message text is entered) */ - public get sendMessageButton(): ChainablePromiseElement< - Promise - > { + public get sendMessageButton() { return $('[data-testid="thread-details-text-area-send-button"]') } /** [Selector] Message not sent icon displayed next to the message */ - public get messageNotSentIcon(): ChainablePromiseElement< - Promise - > { + public get messageNotSentIcon() { return $('[data-testid="message-bubble-not-send-icon"]') } /** [Selector] Delete button visible on open thread (thread details) screen*/ - public get threadDetailScreenDeleteButton(): ChainablePromiseElement< - Promise - > { + public get threadDetailScreenDeleteButton() { return $('[data-testid="right-sidebar-delete-button"]') } /** [Selector] Add contact button visible on open thread (thread details) screen*/ - public get threadDetailScreenAddContactButton(): ChainablePromiseElement< - Promise - > { + public get threadDetailScreenAddContactButton() { return $('[data-testid="right-sidebar-contact-button"]') } /** [Selector] Close button visible on open thread (thread details) screen*/ - public get threadDetailScreenCloseButton(): ChainablePromiseElement< - Promise - > { + public get threadDetailScreenCloseButton() { return $('[data-testid="table-sidebar-close"]') } /** [Selector] Mark as unread button visible on open thread (thread details) screen*/ - public get threadDetailScreenMarkAsUnreadButton(): ChainablePromiseElement< - Promise - > { + public get threadDetailScreenMarkAsUnreadButton() { return $('[data-testid="right-sidebar-mark-as-unread-button"]') } /** [Selector] Single sending status */ - public get sendingStatusIcon(): ChainablePromiseElement< - Promise - > { + public get sendingStatusIcon() { return $('[data-testid="dot"]') } /** [Selector] Single message container */ - public get messageContainer(): ChainablePromiseElement< - Promise - > { + public get messageContainer() { return $('[data-testid="message-bubble-container"]') } /** [Selector] Single message content element*/ - public get messageContent(): ChainablePromiseElement< - Promise - > { + public get messageContent() { return $('[data-testid="message-bubble-message-content"]') } /** Hover over Message content element to display options button*/ @@ -96,18 +71,14 @@ class MessagesConversationPage extends Page { } /** [Selector] Single message options button (...) visible on hover over message. Same purpose as messageDropdownIcon */ - public get messageDropdownButton(): ChainablePromiseElement< - Promise - > { + public get messageDropdownButton() { return this.messageContainer.$( '[data-testid="message-bubble-dropdown-action-button"]' ) } /** [Selector] Single message options icon (...) visible on hover over message*/ - public get messageDropdownIcon(): ChainablePromiseElement< - Promise - > { + public get messageDropdownIcon() { return this.messageContainer.$('[data-testid="icon-More"]') } @@ -118,16 +89,12 @@ class MessagesConversationPage extends Page { } /** Dropdown element displayed after message options button/icon is clicked*/ - public get messageDropdown(): ChainablePromiseElement< - Promise - > { + public get messageDropdown() { return $('[data-testid="dropdown"]') } /** [Selector] Delete message button on message dropodown list */ - public get messageDropdownDeleteButton(): ChainablePromiseElement< - Promise - > { + public get messageDropdownDeleteButton() { return this.messageDropdown.$( '[data-testid="message-bubble-delete-message-button"]' ) @@ -182,37 +149,27 @@ class MessagesConversationPage extends Page { } /** [Selector] Thread details top level element*/ - public get threadDetailsContainer(): ChainablePromiseElement< - Promise - > { + public get threadDetailsContainer() { return $('[data-testid="messages-thread-details"]') } /** [Selector] Browse contacts button*/ - public get browseContactsButton(): ChainablePromiseElement< - Promise - > { + public get browseContactsButton() { return $('[data-testid="new-message-form-browse-contacts"]') } /** [Selector] Conversation recipient name text*/ - public get conversationRecipientNameText(): ChainablePromiseElement< - Promise - > { + public get conversationRecipientNameText() { return $('[data-testid="sidebar-fullname"]') } /** [Selector] Conversation recipient number text*/ - public get conversationRecipientPhoneText(): ChainablePromiseElement< - Promise - > { + public get conversationRecipientPhoneText() { return $('[data-testid="sidebar-phone-number"]') } /** [Selector] Contact search result item*/ - public get contactSearchResultItem(): ChainablePromiseElement< - Promise - > { + public get contactSearchResultItem() { return $('[data-testid="input-select-list-item"]') } /** [Selector] Returns list of multiple elements - contact icon for threads with matching contact in phonebok */ @@ -220,9 +177,7 @@ class MessagesConversationPage extends Page { return this.messageContent.$(`strong*=${text}`) } /** [Selector] Delete message button on message dropodown list */ - public get messageDropdownResendButton(): ChainablePromiseElement< - Promise - > { + public get messageDropdownResendButton() { return this.messageDropdown.$( '[data-testid="message-bubble-resend-message-button"]' ) diff --git a/apps/mudita-center-e2e/src/page-objects/messages-templates.page.ts b/apps/mudita-center-e2e/src/page-objects/messages-templates.page.ts index e07ea5ff4f..ce3fe5c377 100644 --- a/apps/mudita-center-e2e/src/page-objects/messages-templates.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/messages-templates.page.ts @@ -3,25 +3,18 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import ModalPage from "./modal.page" class TemplatesPage extends ModalPage { - public get newTemplateButton(): ChainablePromiseElement< - Promise - > { + public get newTemplateButton() { return $('[data-testid="templates-panel-button"]') } - public get templateTextInputForm(): ChainablePromiseElement< - Promise - > { + public get templateTextInputForm() { return $('[data-testid="template-form-text-filed"]') } - public get saveButton(): ChainablePromiseElement< - Promise - > { + public get saveButton() { return $('[data-testid="template-form-save-button"]') } @@ -31,15 +24,11 @@ class TemplatesPage extends ModalPage { await this.templateTextInputForm.setValue(inputText) } - public get templateCheckbox(): ChainablePromiseElement< - Promise - > { + public get templateCheckbox() { return $('[data-testid="template-checkbox"]') } - public get selectAllTemplatesCheckbox(): ChainablePromiseElement< - Promise - > { + public get selectAllTemplatesCheckbox() { return $('[data-testid="checkbox"]') } diff --git a/apps/mudita-center-e2e/src/page-objects/messages.page.ts b/apps/mudita-center-e2e/src/page-objects/messages.page.ts index b35a16aa9a..1131afbf6d 100644 --- a/apps/mudita-center-e2e/src/page-objects/messages.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/messages.page.ts @@ -3,46 +3,35 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" const backspaceKey = "\ue003" class MessagesPage extends Page { /** [Selector] NEW MESSAGE button selector */ - public get newMessageButton(): ChainablePromiseElement< - Promise - > { + public get newMessageButton() { return $('[data-testid="message-panel-new-message-button"]') } /** [Selector] Message not sent icon displayed on the thread list */ - public get messageNotSentThreadIcon(): ChainablePromiseElement< - Promise - > { + public get messageNotSentThreadIcon() { return $('[data-testid="thread-not-send-message-icon"]') } /** Empty thread list screen: * You don't have any messages yet * Don’t hesitate - let your friends know you’re thinking about them */ - public get threadScreenEmptyList(): ChainablePromiseElement< - Promise - > { + public get threadScreenEmptyList() { return $('[data-testid="messages-empty-thread-list-state"]') } /** [Selector] Thread options icon (...) */ - public get threadDropdownIcon(): ChainablePromiseElement< - Promise - > { + public get threadDropdownIcon() { return $('[data-testid="icon-More"]') } /** [Selector] Thread options icon (...) */ - public get threadDropdownButton(): ChainablePromiseElement< - Promise - > { + public get threadDropdownButton() { return $('[data-testid="thread-row-toggler"]') } /** Click Thread options button*/ @@ -52,9 +41,7 @@ class MessagesPage extends Page { } /** [Selector] Delete conversation button on therad dropodown list */ - public get threadDropdownDeleteButton(): ChainablePromiseElement< - Promise - > { + public get threadDropdownDeleteButton() { return $('[data-testid="dropdown-delete"]') } @@ -77,9 +64,7 @@ class MessagesPage extends Page { } /** [Selector] Thread row element */ - public get threadRow(): ChainablePromiseElement< - Promise - > { + public get threadRow() { return $('[data-testid="message-row"]') } @@ -90,9 +75,7 @@ class MessagesPage extends Page { } /** [Selector] Thread checkbox*/ - public get threadCheckbox(): ChainablePromiseElement< - Promise - > { + public get threadCheckbox() { return $('[data-testid="checkbox"]') } @@ -103,9 +86,7 @@ class MessagesPage extends Page { } /** [Selector] Delete button available when selection manager is active */ - public get selectionManagerIconDelete(): ChainablePromiseElement< - Promise - > { + public get selectionManagerIconDelete() { return $('[data-testid="icon-Delete"]') } @@ -116,16 +97,12 @@ class MessagesPage extends Page { } /** [Selector] Thread details top level element*/ - public get threadDetailsContainer(): ChainablePromiseElement< - Promise - > { + public get threadDetailsContainer() { return $('[data-testid="messages-thread-details"]') } /** [Selector] Mark as read on thread dropdown */ - public get threadDropdownMarkAsReadButton(): ChainablePromiseElement< - Promise - > { + public get threadDropdownMarkAsReadButton() { return $('[data-testid="dropdown-mark-as-read"]') } @@ -138,9 +115,7 @@ class MessagesPage extends Page { } /** [Selector] Text of dropdownMarkAsReadButton, depending on the message status can be 'Mark as read' or 'Mark as unread' */ - public get textOptionMarkAsReadDropdown(): ChainablePromiseElement< - Promise - > { + public get textOptionMarkAsReadDropdown() { return this.threadDropdownMarkAsReadButton.$("p") } /** Returns list of last message text displayed on the conversation list and true/false depending on message unread/read status*/ @@ -172,9 +147,7 @@ class MessagesPage extends Page { return result } /** [Selector] Messages search input */ - public get searchInput(): ChainablePromiseElement< - Promise - > { + public get searchInput() { return $('[data-testid="input-search"]') } @@ -190,21 +163,15 @@ class MessagesPage extends Page { } /** [Selector] Search result overlay list */ - public get searchResultsOverlayList(): ChainablePromiseElement< - Promise - > { + public get searchResultsOverlayList() { return $('[data-testid="input-select-list"]') } /** [Selector] empty search results overlay list */ - public get emptySearchResultsOverlayList(): ChainablePromiseElement< - Promise - > { + public get emptySearchResultsOverlayList() { return this.searchResultsOverlayList.$("li*=No results found") } /** [Selector] See all button on search results overlay */ - public get seeAllSearchResultsButton(): ChainablePromiseElement< - Promise - > { + public get seeAllSearchResultsButton() { return $("button*=See all") } @@ -218,24 +185,18 @@ class MessagesPage extends Page { return $$('[data-testid="avatar-text"]') } /** [Selector] Selection manager- select all checkbox */ - public get selectAllCheckbox(): ChainablePromiseElement< - Promise - > { + public get selectAllCheckbox() { return $('[data-testid="message-panel-selection-manager"]').$( '[type="checkbox"]' ) } /** [Selector] Selection manager- select all checkbox */ - public get selectionManagerDeleteButton(): ChainablePromiseElement< - Promise - > { + public get selectionManagerDeleteButton() { return $('[data-testid="message-panel-selection-manager"]').$("p*=Delete") } /** [Selector] Search results for ' ' text displayed above the thread list*/ - public get searchResultsForText(): ChainablePromiseElement< - Promise - > { + public get searchResultsForText() { return $("h4*=Search results") } @@ -268,16 +229,12 @@ class MessagesPage extends Page { } /** [Selector] Element containing contact number/name displayed on Search results for '' list */ - public get nameFieldOnSearchResultsConversationList(): ChainablePromiseElement< - Promise - > { + public get nameFieldOnSearchResultsConversationList() { return $('[data-testid="name-field"]') } /** [Selector] Templates tab */ - public get templatesTab(): ChainablePromiseElement< - Promise - > { + public get templatesTab() { return $("p*=Templates") } } diff --git a/apps/mudita-center-e2e/src/page-objects/modal-backup-kompakt.page.ts b/apps/mudita-center-e2e/src/page-objects/modal-backup-kompakt.page.ts new file mode 100644 index 0000000000..9d5714b246 --- /dev/null +++ b/apps/mudita-center-e2e/src/page-objects/modal-backup-kompakt.page.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { OverviewPage } from "./overview.page" + +class ModalBackupKompaktPage extends OverviewPage { + public get createBackupButton() { + return $('[data-testid="primary-button-backupcreate-backup-button"]') + } + public get createBackupProceedNext() { + return $('[data-testid="backup-features-modal-create-action"]') + } + + public get contactList() { + return $$('[data-testid="backup-features-modal-element-active"]')[0] + } + public get callLog() { + return $$('[data-testid="backup-features-modal-element-active"]')[1] + } + public get backupModalTitle() { + return $('[data-testid="backup-features-modal-title"]') + } + public get backupModalDescription() { + return $('[data-testid="backup-features-modal-description"]') + } + public get backupModalCancel() { + return $('[data-testid="backup-features-modal-cancel-action"]') + } + + public get backupModalClose() { + return $('[data-testid="modal-close-button-icon-button"]') + } + + public get createBackupPasswordModalTitle() { + return $('[data-testid="predefined-backup-password-title"]') + } + + public get createBackupPasswordOptionalText() { + return $('[data-testid="predefined-backup-password-title"] span') + } + public get createBackupPasswordModalDescription() { + return $('[data-testid="predefined-backup-password-description"]') + } + + public get createBackupPasswordModalDescriptionMore() { + return $('[data-testid="predefined-backup-password-description"] span') + } + + public get createBackupPasswordPlaceholder() { + return $('[data-testid="predefined-backup-password-placeholder"]') + } + + public get createBackupPasswordRepeatPlaceholder() { + return $('[data-testid="predefined-backup-password-repeat-placeholder"]') + } + + public get createBackupPasswordConfirm() { + return $('[data-testid="predefined-backup-password-confirm-button"]') + } + + public get createBackupPasswordSkip() { + return $('[data-testid="predefined-backup-password-skip-button"]') + } + + public get createBackupPasswordClose() { + return $('[data-testid="modal-close-button-icon-button"]') + } + + public get inputPassword() { + return $$('[data-testid="interactive-text-input-input"]')[0] + } + + public get repeatInputPassword() { + return $$('[data-testid="interactive-text-input-input"]')[1] + } + + public get unhidePasswordIcon() { + return $('[data-testid="icon-password-hide"]') + } + + public get hidePasswordIcon() { + return $('[data-testid="icon-password-show"]') + } + + public get passwordsDoNotMatch() { + return $('[data-testid="interactive-text-input-error-text"]') + } +} +export default new ModalBackupKompaktPage() diff --git a/apps/mudita-center-e2e/src/page-objects/modal-backup-restore.page.ts b/apps/mudita-center-e2e/src/page-objects/modal-backup-restore.page.ts index 24da798641..eb729c8b94 100644 --- a/apps/mudita-center-e2e/src/page-objects/modal-backup-restore.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/modal-backup-restore.page.ts @@ -3,19 +3,14 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class ModalBackupRestorePage extends Page { - public get failModalIcon(): ChainablePromiseElement< - Promise - > { + public get failModalIcon() { return $('[data-testid="icon-Fail"]') } - public get checkCircleIcon(): ChainablePromiseElement< - Promise - > { + public get checkCircleIcon() { return $('[data-testid="icon-CheckCircle"]') } @@ -23,45 +18,31 @@ class ModalBackupRestorePage extends Page { return $$('[data-testid="restore-available-backup-modal-body-row"]') } - public get restoreButton(): ChainablePromiseElement< - Promise - > { + public get restoreButton() { return $('[data-testid="modal-action-button"]*=Restore') } - public get restorePasswordInput(): ChainablePromiseElement< - Promise - > { + public get restorePasswordInput() { return $('[name="secretKey"]') } - public get restoreSubmitButton(): ChainablePromiseElement< - Promise - > { + public get restoreSubmitButton() { return $('[type="submit"]*=Confirm') } //BACKUP modal objects - public get createBackupModalButton(): ChainablePromiseElement< - Promise - > { + public get createBackupModalButton() { return $('[data-testid="modal-action-button"]') } - public get backupPasswordFirstInput(): ChainablePromiseElement< - Promise - > { + public get backupPasswordFirstInput() { return $('[data-testid="backup-first-input"]') } - public get backupPasswordSecondInput(): ChainablePromiseElement< - Promise - > { + public get backupPasswordSecondInput() { return $('[data-testid="backup-second-input"]') } - public get backupSubmitButton(): ChainablePromiseElement< - Promise - > { + public get backupSubmitButton() { return $('[data-testid="backup-submit-button"]') } } diff --git a/apps/mudita-center-e2e/src/page-objects/modal-contacts.page.ts b/apps/mudita-center-e2e/src/page-objects/modal-contacts.page.ts index 0883164726..4e3a30733a 100644 --- a/apps/mudita-center-e2e/src/page-objects/modal-contacts.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/modal-contacts.page.ts @@ -3,13 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class ModalContacts extends Page { - public get iconClose(): ChainablePromiseElement< - Promise - > { + public get iconClose() { return $('[data-testid="icon-Close"]') } async buttonCloseClick() { @@ -21,27 +18,19 @@ class ModalContacts extends Page { } } - public get modalContent(): ChainablePromiseElement< - Promise - > { + public get modalContent() { return $('[data-testid="delete-modal-content"]') } - public get textOnModal(): ChainablePromiseElement< - Promise - > { + public get textOnModal() { return $('[data-testid="delete-modal-content"]').$("p") } - public get iconDelete(): ChainablePromiseElement< - Promise - > { + public get iconDelete() { return $('[data-testid="icon-DeleteBig"]') } - public get buttonConfirmDelete(): ChainablePromiseElement< - Promise - > { + public get buttonConfirmDelete() { return $('[data-testid="modal-action-button"]*=Delete') } async buttonConfirmDeleteClick() { @@ -49,28 +38,22 @@ class ModalContacts extends Page { await this.buttonConfirmDelete.click() } - public get buttonCancel(): ChainablePromiseElement< - Promise - > { + public get buttonCancel() { return $('[data-testid="close-bottom-button"]') } async cancelModalButtonClick() { await this.buttonCancel.click() } - public get buttonImport(): ChainablePromiseElement< - Promise - > { + public get buttonImport() { return $('[data-testid="modal-action-button"]*=Import') } - public get textSavingCompleted(): ChainablePromiseElement< - Promise - > { + public get textSavingCompleted() { return $("h4*=Saving completed") } - public get buttonOK(): ChainablePromiseElement> { + public get buttonOK() { return $('[data-testid="modal-action-button"]*=OK') } } diff --git a/apps/mudita-center-e2e/src/page-objects/modal-general.page.ts b/apps/mudita-center-e2e/src/page-objects/modal-general.page.ts index b51f2a4494..35a106955a 100644 --- a/apps/mudita-center-e2e/src/page-objects/modal-general.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/modal-general.page.ts @@ -3,13 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class ModalGeneralPage extends Page { - public get closeIcon(): ChainablePromiseElement< - Promise - > { + public get closeIcon() { return $('[data-testid="icon-Close"]') } async closeModalButtonClick() { @@ -20,15 +17,11 @@ class ModalGeneralPage extends Page { console.log(error) } } - public get modalHeader(): ChainablePromiseElement< - Promise - > { + public get modalHeader() { return $('[data-testid="modal-header"]') } - public get updateAvailableModalCloseButton(): ChainablePromiseElement< - Promise - > { + public get updateAvailableModalCloseButton() { return this.modalHeader.$('[data-testid="icon-Close"]') } @@ -42,9 +35,7 @@ class ModalGeneralPage extends Page { console.log(error) } } - public get closeModalBackgroundUpdateAvailableFailed(): ChainablePromiseElement< - Promise - > { + public get closeModalBackgroundUpdateAvailableFailed() { return $('[data-testid="update-os-flow-check-for-update-failed-modal"]').$( '[data-testid="icon-Close"]' ) @@ -61,15 +52,11 @@ class ModalGeneralPage extends Page { } } - public get updateNotAvailableModal(): ChainablePromiseElement< - Promise - > { + public get updateNotAvailableModal() { return $('[data-testid="update-os-flow-update-not-available-modal"]') } - public get closeModalButton(): ChainablePromiseElement< - Promise - > { + public get closeModalButton() { return $('[data-testid="close-modal-button"]') } diff --git a/apps/mudita-center-e2e/src/page-objects/modal-messages.page.ts b/apps/mudita-center-e2e/src/page-objects/modal-messages.page.ts index 47b912ea56..559ee6de6a 100644 --- a/apps/mudita-center-e2e/src/page-objects/modal-messages.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/modal-messages.page.ts @@ -3,13 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class ModalMessages extends Page { - public get iconClose(): ChainablePromiseElement< - Promise - > { + public get iconClose() { return $('[data-testid="icon-Close"]') } async buttonCloseClick() { @@ -21,9 +18,7 @@ class ModalMessages extends Page { } } - public get confirmDeleteButton(): ChainablePromiseElement< - Promise - > { + public get confirmDeleteButton() { return $('[data-testid="modal-action-button"]*=Delete') } async clickConfirmDeleteButton() { diff --git a/apps/mudita-center-e2e/src/page-objects/news.page.ts b/apps/mudita-center-e2e/src/page-objects/news.page.ts index 5b91e12e42..832baaeeed 100644 --- a/apps/mudita-center-e2e/src/page-objects/news.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/news.page.ts @@ -3,7 +3,6 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class NewsPage extends Page { @@ -15,6 +14,10 @@ class NewsPage extends Page { return $("p*=More news") } + public get moreNewsButtonHref() { + return $('a[href="https://www.mudita.com/#news"]') + } + public get newsCardElements() { return $$('[data-testid="news-card"]') } diff --git a/apps/mudita-center-e2e/src/page-objects/overview-kompakt.page.ts b/apps/mudita-center-e2e/src/page-objects/overview-kompakt.page.ts index 79ccf96a76..907decc013 100644 --- a/apps/mudita-center-e2e/src/page-objects/overview-kompakt.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/overview-kompakt.page.ts @@ -31,7 +31,7 @@ class OverviewKompaktPage extends OverviewPage { } public get backupInfo() { - return $(`//div[@data-testid="block-box-backup"]//p`) + return $('div[data-testid="block-box-backup"] p') } public get serialNumberLabel() { diff --git a/apps/mudita-center-e2e/src/page-objects/overview.page.ts b/apps/mudita-center-e2e/src/page-objects/overview.page.ts index ece1accf18..e7e199f245 100644 --- a/apps/mudita-center-e2e/src/page-objects/overview.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/overview.page.ts @@ -55,7 +55,7 @@ export class OverviewPage extends Page { } public get createBackupButton() { - return $('//button[@type="button" and .//span[text()="Create backup"]]') + return $('[data-testid="primary-button-backupcreate-backup-button"]') } public get restoreBackupButton() { diff --git a/apps/mudita-center-e2e/src/page-objects/tabs.page.ts b/apps/mudita-center-e2e/src/page-objects/tabs.page.ts index 795cb7a1d1..3d712b22ca 100644 --- a/apps/mudita-center-e2e/src/page-objects/tabs.page.ts +++ b/apps/mudita-center-e2e/src/page-objects/tabs.page.ts @@ -3,7 +3,6 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ChainablePromiseElement } from "webdriverio" import Page from "./page" class NavigationTabs extends Page { @@ -53,8 +52,10 @@ class NavigationTabs extends Page { return $('[data-testid="help-menu-button"]') } - async helpTabClick() { - await this.helpTab.click() + public async openHelpPage() { + const helpTab = await this.helpTab; + await helpTab.waitForDisplayed({ timeout: 15000 }); + await helpTab.click(); } } diff --git a/apps/mudita-center-e2e/src/specs/help/contact-support-unhappy-path.ts b/apps/mudita-center-e2e/src/specs/help/contact-support-unhappy-path.ts new file mode 100644 index 0000000000..ef933d7ef9 --- /dev/null +++ b/apps/mudita-center-e2e/src/specs/help/contact-support-unhappy-path.ts @@ -0,0 +1,117 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import NavigationTabs from "../../page-objects/tabs.page" +import HelpPage from "../../page-objects/help.page" +import HomePage from "../../page-objects/home.page" +import HelpModalPage from "../../page-objects/help-modal.page" + +describe("Contact Support - Unhappy Path", () => { + before(async () => { + const notNowButton = await HomePage.notNowButton + await notNowButton.waitForDisplayed() + await notNowButton.click() + }) + + it("Open Mudita Help Center and open Contact Support Modal", async () => { + const helpTab = NavigationTabs.helpTab + await helpTab.click() + + const helpMainFooterContactSupportButton = + HelpPage.helpMainFooterContactSupportButton + await helpMainFooterContactSupportButton.click() + }) + + it("Check contents of Contact Form", async () => { + const wholeModal = HelpModalPage.wholeModal + await expect(wholeModal).toBeDisplayed() + + const closeModalButton = HelpModalPage.closeModalButton + await expect(closeModalButton).toBeDisplayed() + + const modalHeader = HelpModalPage.modalHeader + await expect(modalHeader).toBeDisplayed() + + const iconSupport = HelpModalPage.iconSupport + await expect(iconSupport).toBeDisplayed() + + const modalTitle = HelpModalPage.modalTitle + await expect(modalTitle).toBeDisplayed() + await expect(modalTitle).toHaveText("Mudita Center Support") + + const modalSubtitle = HelpModalPage.modalSubtitle + await expect(modalSubtitle).toBeDisplayed() + await expect(modalSubtitle).toHaveText( + "Contact Mudita support team and we will do our best to help you resolve your issues." + ) + + const emailLabel = HelpModalPage.emailLabel + await expect(emailLabel).toHaveText("Email") + const emailInput = HelpModalPage.emailInput + await expect(emailInput).toBeDisplayed() + + const messageLabel = HelpModalPage.messageLabel + await expect(messageLabel).toHaveText("Message (optional)") + const descriptionInput = HelpModalPage.descriptionInput + await expect(descriptionInput).toBeDisplayed() + + const attachedFilesLabel = HelpModalPage.attachedFilesLabel + const attachedFilesSubText = HelpModalPage.attachedFilesSubText + const singleAttachment = HelpModalPage.singleAttachment + const iconAttachment = HelpModalPage.iconAttachment + const currentDateZipFile = HelpModalPage.currentDateZipFile + await expect(singleAttachment).toBeDisplayed() + await expect(iconAttachment).toBeDisplayed() + await expect(currentDateZipFile).toBeDisplayed() //checks if zipfile has current date (date of sending the logs is the same as zip file name) + await expect(attachedFilesLabel).toHaveText("Attached files") + await expect(attachedFilesSubText).toHaveText( + "The attached files will help us resolve your problem" + ) + }) + + it("Check if SEND button is present, has proper name and is disabled", async () => { + const sendButton = HelpModalPage.sendButton + const sendButtonLabel = HelpModalPage.sendButtonLabel + await expect(sendButton).toBeDisplayed() + await expect(sendButtonLabel).toHaveText("SEND") + await expect(sendButton).toBeDisabled() + }) + + it("Try to Send form without any input", async () => { + const sendButton = HelpModalPage.sendButton + await expect(sendButton).toBeDisabled() + }) + + it("Verify e-mail without @ character", async () => { + const emailInput = HelpModalPage.emailInput + await emailInput.setValue("emailtest.com") + const emailWarning = "Email is invalid" + const invalidEmailTextElement = HelpModalPage.invalidEmailTextElement + await expect(invalidEmailTextElement).toHaveText(emailWarning) + }) + + it("Check e-mail with @@ characters", async () => { + const emailInput = HelpModalPage.emailInput + await emailInput.setValue("email@@test.com") + const emailWarning = "Email is invalid" + const invalidEmailTextElement = HelpModalPage.invalidEmailTextElement + await expect(invalidEmailTextElement).toHaveText(emailWarning) + }) + + it("Enter correct e-mail to check if error message dissappears", async () => { + const emailInput = HelpModalPage.emailInput + await emailInput.setValue("email@test.com") + const invalidEmailTextElement = HelpModalPage.invalidEmailTextElement + await expect(invalidEmailTextElement).not.toBeDisplayed() + }) + + it("Check e-mail with , character", async () => { + const emailInput = HelpModalPage.emailInput + await emailInput.setValue("email@test,com") + const emailWarning = "Email is invalid" + const invalidEmailTextElement = HelpModalPage.invalidEmailTextElement + await expect(invalidEmailTextElement).toHaveText(emailWarning) + }) +}) diff --git a/apps/mudita-center-e2e/src/specs/help/help-link-inside-container.ts b/apps/mudita-center-e2e/src/specs/help/help-link-inside-container.ts new file mode 100644 index 0000000000..034ebfbb27 --- /dev/null +++ b/apps/mudita-center-e2e/src/specs/help/help-link-inside-container.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import NavigationTabs from "../../page-objects/tabs.page" +import HelpArticlePage from "../../page-objects/help-article.page" +import HomePage from "../../page-objects/home.page" + +describe("Help - Link inside container", () => { + before(async () => { + const notNowButton = await HomePage.notNowButton + await notNowButton.waitForDisplayed() + await notNowButton.click() + }) + + it("Open Help window", async () => { + const helpTab = await NavigationTabs.helpTab + await helpTab.waitForDisplayed({ timeout: 15000 }) + await helpTab.click() + }) + + it("Open Pure Category in Help", async () => { + const pureCategory = HelpArticlePage.helpCategories[1] + await expect(pureCategory).toBeDisplayed() + await pureCategory.click() + }) + + it("Open second article, check if there are two containers and click on the link inside first container", async () => { + const secondArticle = HelpArticlePage.helpArticleItems[1] + await expect(secondArticle).toBeDisplayed() + await secondArticle.click() + + //check if there are two containers in the Article + const contentBlocks = HelpArticlePage.helpArticleContentBlocks + await expect(contentBlocks).toBeElementsArrayOfSize(2) + + //open link from the first container + const connectYourDeviceLinkFirst = HelpArticlePage.connectYourDeviceLinks[0] + await expect(connectYourDeviceLinkFirst).toBeDisplayed() + await expect(connectYourDeviceLinkFirst).toHaveText("connect your device") + await connectYourDeviceLinkFirst.click() + }) + + it("Check if user is redirected to article from the link", async () => { + const helpArticleTitle = HelpArticlePage.helpArticleTitle + await expect(helpArticleTitle).toBeDisplayed() + await expect(helpArticleTitle).toHaveText( + "How to connect Mudita devices to Mudita Center" + ) + }) + + it("Return to the main article", async () => { + const helpArticleBackButton = await HelpArticlePage.helpArticleBackButton + await expect(helpArticleBackButton).toBeClickable() + helpArticleBackButton.click() + + const helpArticleTitle = HelpArticlePage.helpArticleTitle + await expect(helpArticleTitle).toBeDisplayed() + await expect(helpArticleTitle).toHaveText("How to delete files from Pure") + }) +}) diff --git a/apps/mudita-center-e2e/src/specs/help/help-section-check-offline.e2e.ts b/apps/mudita-center-e2e/src/specs/help/help-section-check-offline.e2e.ts new file mode 100644 index 0000000000..589664404a --- /dev/null +++ b/apps/mudita-center-e2e/src/specs/help/help-section-check-offline.e2e.ts @@ -0,0 +1,249 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import dns from "node:dns" +import NavigationTabs from "../../page-objects/tabs.page" +import HelpPage from "../../page-objects/help.page" +import HelpArticlePage from "../../page-objects/help-article.page" +import HomePage from "../../page-objects/home.page" +import testsHelper from "../../helpers/tests.helper" + +describe("Check Help window", () => { + before(async () => { + dns.setDefaultResultOrder("ipv4first") + await browser.throttle("offline") + + // Switch to offline mode before starting the tests + await browser.setNetworkConditions({ + offline: true, + latency: 0, + download_throughput: 0, + upload_throughput: 0, + }) + + // Add a small delay to ensure network conditions are applied + await browser.pause(1000) + + // Verify network conditions + const isOnline = await testsHelper.isOnline() + await expect(isOnline).toBeFalsy() + + const notNowButton = await HomePage.notNowButton + await notNowButton.waitForDisplayed() + await notNowButton.click() + }) + + it("Open Help window", async () => { + const helpTab = await NavigationTabs.helpTab + await helpTab.waitForDisplayed({ timeout: 15000 }) + await helpTab.click() + + //Check window title + const helpTabTitle = await HelpPage.helpTabTitle + await helpTabTitle.waitForDisplayed({ timeout: 15000 }) + await expect(helpTabTitle).toHaveText("Mudita Help Center") + + //Verify welcome message + const helpMainHeader = await HelpPage.helpMainHeader + await expect(helpMainHeader).toHaveText("Welcome! How can we help you?") + + //Verify welcome paragraph + const helpMainSubHeader = await HelpPage.helpMainSubHeader + await expect(helpMainSubHeader).toHaveText( + "Browse our selection of how-to and troubleshooting guides" + ) + + //Verify search bar + const iconSearch = await HelpPage.iconSearch + await expect(iconSearch).toBeDisplayed() + + //Verify search bar icon + const helpSearchInput = await HelpPage.helpSearchInput + await expect(helpSearchInput).toBeDisplayed() + + //Verify placeholder + await expect(helpSearchInput).toHaveAttrContaining( + "placeholder", + "Search topics" + ) + + //Verify main section title + const helpCategoriesTitle = await HelpPage.helpCategoriesTitle + await expect(helpCategoriesTitle).toHaveText( + "Which device are you using with Mudita Center?" + ) + + //Section tabs + const helpCategoriesList = await HelpPage.helpCategoriesList + await expect(helpCategoriesList).toBeDisplayed() + + const helpCategoriesListItems = await HelpPage.helpCategoriesListItems + await expect(helpCategoriesListItems).toBeElementsArrayOfSize({ gte: 1 }) + + //Active section tab + await expect(helpCategoriesListItems[0]).toBeDisplayed() // Added this line to ensure the first element is displayed + + await expect(helpCategoriesListItems[0]).toHaveElementClassContaining( + "active" + ) + + const activeTabColor = await helpCategoriesListItems[0].getCSSProperty( + "color" + ) + await expect(activeTabColor.value).toBe("rgba(0,0,0,1)") + + const activeTabBackground = await helpCategoriesListItems[0].getCSSProperty( + "background-color" + ) + await expect(activeTabBackground.value).toBe("rgba(237,237,237,1)") + + //Hover on section tabs + await helpCategoriesListItems[1].moveTo() + const hoverTabColor = await helpCategoriesListItems[0].getCSSProperty( + "color" + ) + await expect(hoverTabColor.value).toBe("rgba(0,0,0,1)") + const hoverTabBackground = await helpCategoriesListItems[0].getCSSProperty( + "background-color" + ) + await expect(hoverTabBackground.value).toBe("rgba(237,237,237,1)") + }) + + it("Verify Harmony Section titles", async () => { + //Verify all items + const helpSubCategoriesListItems = await HelpPage.helpSubCategoriesListItems + await expect(helpSubCategoriesListItems).toBeElementsArrayOfSize({ gte: 1 }) + + //Verify left column + const helpSubCategoriesListItemsLeftColumn = + await HelpPage.helpSubCategoriesListItemsLeftColumn + await expect(helpSubCategoriesListItemsLeftColumn).toBeElementsArrayOfSize({ + gte: 1, + }) + + //Verify right column + const helpSubCategoriesListItemsRightColumn = + await HelpPage.helpSubCategoriesListItemsRightColumn + await expect(helpSubCategoriesListItemsRightColumn).toBeElementsArrayOfSize( + { gte: 1 } + ) + + //Every sub category should not be empty + const helpSubCategoryArticlesListItemTitles = + await helpSubCategoriesListItems.map((element) => { + return element + .$('[data-testid="help-subcategories-list-item-title"]') + .getText() + }) + await expect( + helpSubCategoryArticlesListItemTitles.length + ).toBeGreaterThanOrEqual(1) + + //List of articles should not be empty in any of the categories + let helpSubCategoriesListItem + for await (helpSubCategoriesListItem of helpSubCategoriesListItems) { + await expect( + helpSubCategoriesListItem.$$( + '[data-testid="help-subcategory-articles-list-item"]' + ) + ).toBeElementsArrayOfSize({ gte: 1 }) + } + }) + it("Search for questions and verify results", async () => { + const helpSearchInput = await HelpPage.helpSearchInput + await helpSearchInput.setValue("How to do factory reset on Pure") + + //Verify quick search results + const helpSearchResults = await HelpPage.helpSearchResults + await expect(helpSearchResults).toBeDisplayed() + const helpSearchResultsParagraph = await HelpPage.helpSearchResultsParagraph + await expect(helpSearchResultsParagraph).toBeDisplayed() + await expect(helpSearchResultsParagraph).toHaveText("Quick Links") + //List should not be empty, bigger than 1 + const helpSearchResultsItems = await HelpPage.helpSearchResultsItems + await expect(helpSearchResultsItems).toBeElementsArrayOfSize({ gte: 1 }) + //Click first article + helpSearchResultsItems[0].click() + }) + it("Check first article", async () => { + //Check window title + const helpTabTitle = await HelpPage.helpTabTitle + await helpTabTitle.waitForDisplayed({ timeout: 15000 }) + await expect(helpTabTitle).toHaveText("Mudita Help Center") + + //Check back button + const helpArticleBackButton = await HelpArticlePage.helpArticleBackButton + await expect(helpArticleBackButton).toBeClickable() + + //Check article title + const helpArticleTitle = await HelpArticlePage.helpArticleTitle + await expect(helpArticleTitle).toHaveText("How to do factory reset on Pure") + + //Check article warning + const helpArticleWarningIcon = await HelpArticlePage.helpArticleWarningIcon + await expect(helpArticleWarningIcon).toBeDisplayed() + + const helpArticleWarning = await HelpArticlePage.helpArticleWarning + await expect(helpArticleWarning).toHaveTextContaining( + "This will delete everything on your phone!" + ) + + //Check article content + const helpArticleContent = await HelpArticlePage.helpArticleContent + await expect(helpArticleContent).toBeDisplayed() + const helpArticleContentBlocks = + await HelpArticlePage.helpArticleContentBlocks + await expect(helpArticleContentBlocks).toBeElementsArrayOfSize({ gte: 2 }) + await expect( + HelpArticlePage.getHelpArticleContentBlockTitle(0) + ).toHaveTextContaining("If your Pure is locked:") + await expect( + HelpArticlePage.getHelpArticleContentBlockText(0) + ).toHaveTextContaining( + "Turn off your Pure, hold down the right selection key > select Yes" + ) + + //Check article helpful section + const helpArticleFeedbackYesButton = + await HelpArticlePage.helpArticleFeedbackYesButton + await expect(helpArticleFeedbackYesButton).toBeDisplayed() + await expect(helpArticleFeedbackYesButton).toBeClickable() + + const helpArticleFeedbackNoButton = + await HelpArticlePage.helpArticleFeedbackNoButton + await expect(helpArticleFeedbackNoButton).toBeDisplayed() + await expect(helpArticleFeedbackNoButton).toBeClickable() + + const helpArticleFooter = await HelpArticlePage.helpArticleFooter + await helpArticleFooter.scrollIntoView() + + const helpArticleFooterTitle = await HelpArticlePage.helpArticleFooterTitle + await expect(helpArticleFooterTitle).toHaveText( + "Need more help?\nVisit our Support Website" + ) + + const helpArticleFooterVisitSupportButton = + await HelpArticlePage.helpArticleFooterVisitSupportButton + await expect(helpArticleFooterVisitSupportButton).toHaveText( + "VISIT SUPPORT WEBSITE" + ) + + helpArticleBackButton.click() + }) + it("Verify you are back in active first category", async () => { + const helpCategoriesListItems = await HelpPage.helpCategoriesListItems + + // Ensure that the helpCategoriesListItems array has at least one element + await expect(helpCategoriesListItems).toBeElementsArrayOfSize({ gte: 1 }) + + // Check if the first category item is displayed + await expect(helpCategoriesListItems[0]).toBeDisplayed() + + // Verify the first item has the 'active' class + await expect(helpCategoriesListItems[0]).toHaveElementClassContaining( + "active" + ) + }) +}) diff --git a/apps/mudita-center-e2e/src/specs/help/help-section-check.e2e.ts b/apps/mudita-center-e2e/src/specs/help/help-section-check.e2e.ts new file mode 100644 index 0000000000..a659a415d7 --- /dev/null +++ b/apps/mudita-center-e2e/src/specs/help/help-section-check.e2e.ts @@ -0,0 +1,229 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import NavigationTabs from "../../page-objects/tabs.page" +import HelpPage from "../../page-objects/help.page" +import HelpArticlePage from "../../page-objects/help-article.page" +import HomePage from "../../page-objects/home.page" + +describe("Check Help window", () => { + before(async () => { + const notNowButton = await HomePage.notNowButton + await notNowButton.waitForDisplayed() + await notNowButton.click() + }) + + it("Open Help window", async () => { + const helpTab = await NavigationTabs.helpTab + await helpTab.waitForDisplayed({ timeout: 15000 }) + await helpTab.click() + + //Check window title + const helpTabTitle = await HelpPage.helpTabTitle + await helpTabTitle.waitForDisplayed({ timeout: 15000 }) + await expect(helpTabTitle).toHaveText("Mudita Help Center") + + //Verify welcome message + const helpMainHeader = await HelpPage.helpMainHeader + await expect(helpMainHeader).toHaveText("Welcome! How can we help you?") + + //Verify welcome paragraph + const helpMainSubHeader = await HelpPage.helpMainSubHeader + await expect(helpMainSubHeader).toHaveText( + "Browse our selection of how-to and troubleshooting guides" + ) + + //Verify search bar + const iconSearch = await HelpPage.iconSearch + await expect(iconSearch).toBeDisplayed() + + //Verify search bar icon + const helpSearchInput = await HelpPage.helpSearchInput + await expect(helpSearchInput).toBeDisplayed() + + //Verify placeholder + await expect(helpSearchInput).toHaveAttrContaining( + "placeholder", + "Search topics" + ) + + //Verify main section title + const helpCategoriesTitle = await HelpPage.helpCategoriesTitle + await expect(helpCategoriesTitle).toHaveText( + "Which device are you using with Mudita Center?" + ) + + //Section tabs + const helpCategoriesList = await HelpPage.helpCategoriesList + await expect(helpCategoriesList).toBeDisplayed() + + const helpCategoriesListItems = await HelpPage.helpCategoriesListItems + await expect(helpCategoriesListItems).toBeElementsArrayOfSize({ gte: 1 }) + + //Active section tab + await expect(helpCategoriesListItems[0]).toBeDisplayed() // Added this line to ensure the first element is displayed + + await expect(helpCategoriesListItems[0]).toHaveElementClassContaining( + "active" + ) + + const activeTabColor = await helpCategoriesListItems[0].getCSSProperty( + "color" + ) + await expect(activeTabColor.value).toBe("rgba(0,0,0,1)") + + const activeTabBackground = await helpCategoriesListItems[0].getCSSProperty( + "background-color" + ) + await expect(activeTabBackground.value).toBe("rgba(237,237,237,1)") + + //Hover on section tabs + await helpCategoriesListItems[1].moveTo() + const hoverTabColor = await helpCategoriesListItems[0].getCSSProperty( + "color" + ) + await expect(hoverTabColor.value).toBe("rgba(0,0,0,1)") + const hoverTabBackground = await helpCategoriesListItems[0].getCSSProperty( + "background-color" + ) + await expect(hoverTabBackground.value).toBe("rgba(237,237,237,1)") + }) + + it("Verify Harmony Section titles", async () => { + //Verify all items + const helpSubCategoriesListItems = await HelpPage.helpSubCategoriesListItems + await expect(helpSubCategoriesListItems).toBeElementsArrayOfSize({ gte: 1 }) + + //Verify left column + const helpSubCategoriesListItemsLeftColumn = + await HelpPage.helpSubCategoriesListItemsLeftColumn + await expect(helpSubCategoriesListItemsLeftColumn).toBeElementsArrayOfSize({ + gte: 1, + }) + + //Verify right column + const helpSubCategoriesListItemsRightColumn = + await HelpPage.helpSubCategoriesListItemsRightColumn + await expect(helpSubCategoriesListItemsRightColumn).toBeElementsArrayOfSize( + { gte: 1 } + ) + + //Every sub category should not be empty + const helpSubCategoryArticlesListItemTitles = + await helpSubCategoriesListItems.map((element) => { + return element + .$('[data-testid="help-subcategories-list-item-title"]') + .getText() + }) + await expect( + helpSubCategoryArticlesListItemTitles.length + ).toBeGreaterThanOrEqual(1) + + //List of articles should not be empty in any of the categories + let helpSubCategoriesListItem + for await (helpSubCategoriesListItem of helpSubCategoriesListItems) { + await expect( + helpSubCategoriesListItem.$$( + '[data-testid="help-subcategory-articles-list-item"]' + ) + ).toBeElementsArrayOfSize({ gte: 1 }) + } + }) + it("Search for questions and verify results", async () => { + const helpSearchInput = await HelpPage.helpSearchInput + await helpSearchInput.setValue("How to do factory reset on Pure") + + //Verify quick search results + const helpSearchResults = await HelpPage.helpSearchResults + await expect(helpSearchResults).toBeDisplayed() + const helpSearchResultsParagraph = await HelpPage.helpSearchResultsParagraph + await expect(helpSearchResultsParagraph).toBeDisplayed() + await expect(helpSearchResultsParagraph).toHaveText("Quick Links") + //List should not be empty, bigger than 1 + const helpSearchResultsItems = await HelpPage.helpSearchResultsItems + await expect(helpSearchResultsItems).toBeElementsArrayOfSize({ gte: 1 }) + //Click first article + helpSearchResultsItems[0].click() + }) + it("Check first article", async () => { + //Check window title + const helpTabTitle = await HelpPage.helpTabTitle + await helpTabTitle.waitForDisplayed({ timeout: 15000 }) + await expect(helpTabTitle).toHaveText("Mudita Help Center") + + //Check back button + const helpArticleBackButton = await HelpArticlePage.helpArticleBackButton + await expect(helpArticleBackButton).toBeClickable() + + //Check article title + const helpArticleTitle = await HelpArticlePage.helpArticleTitle + await expect(helpArticleTitle).toHaveText("How to do factory reset on Pure") + + //Check article warning + const helpArticleWarningIcon = await HelpArticlePage.helpArticleWarningIcon + await expect(helpArticleWarningIcon).toBeDisplayed() + + const helpArticleWarning = await HelpArticlePage.helpArticleWarning + await expect(helpArticleWarning).toHaveTextContaining( + "This will delete everything on your phone!" + ) + + //Check article content + const helpArticleContent = await HelpArticlePage.helpArticleContent + await expect(helpArticleContent).toBeDisplayed() + const helpArticleContentBlocks = + await HelpArticlePage.helpArticleContentBlocks + await expect(helpArticleContentBlocks).toBeElementsArrayOfSize({ gte: 2 }) + await expect( + HelpArticlePage.getHelpArticleContentBlockTitle(0) + ).toHaveTextContaining("If your Pure is locked:") + await expect( + HelpArticlePage.getHelpArticleContentBlockText(0) + ).toHaveTextContaining( + "Turn off your Pure, hold down the right selection key > select Yes" + ) + + //Check article helpful section + const helpArticleFeedbackYesButton = + await HelpArticlePage.helpArticleFeedbackYesButton + await expect(helpArticleFeedbackYesButton).toBeDisplayed() + await expect(helpArticleFeedbackYesButton).toBeClickable() + + const helpArticleFeedbackNoButton = + await HelpArticlePage.helpArticleFeedbackNoButton + await expect(helpArticleFeedbackNoButton).toBeDisplayed() + await expect(helpArticleFeedbackNoButton).toBeClickable() + + const helpArticleFooter = await HelpArticlePage.helpArticleFooter + await helpArticleFooter.scrollIntoView() + + const helpArticleFooterTitle = await HelpArticlePage.helpArticleFooterTitle + await expect(helpArticleFooterTitle).toHaveText( + "Need more help?\nVisit our Support Website" + ) + + const helpArticleFooterVisitSupportButton = + await HelpArticlePage.helpArticleFooterVisitSupportButton + await expect(helpArticleFooterVisitSupportButton).toHaveText( + "VISIT SUPPORT WEBSITE" + ) + + helpArticleBackButton.click() + }) + it("Verify you are back in active first category", async () => { + const helpCategoriesListItems = await HelpPage.helpCategoriesListItems + + // Ensure that the helpCategoriesListItems array has at least one element + await expect(helpCategoriesListItems).toBeElementsArrayOfSize({ gte: 1 }) + + // Check if the first category item is displayed + await expect(helpCategoriesListItems[0]).toBeDisplayed() + + // Verify the first item has the 'active' class + await expect(helpCategoriesListItems[0]).toHaveElementClassContaining( + "active" + ) + }) +}) diff --git a/apps/mudita-center-e2e/src/specs/help/help-section-search-noresults.e2e.ts b/apps/mudita-center-e2e/src/specs/help/help-section-search-noresults.e2e.ts new file mode 100644 index 0000000000..50ee4379c1 --- /dev/null +++ b/apps/mudita-center-e2e/src/specs/help/help-section-search-noresults.e2e.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import NavigationTabs from "../../page-objects/tabs.page" +import HelpPage from "../../page-objects/help.page" +import HomePage from "../../page-objects/home.page" + +describe("Check Help search for no results", () => { + before(async () => { + //Click Not Now button + await HomePage.clickNotNowButton() + //Open Help page + await NavigationTabs.openHelpPage() + }) + + it("Search for not existing article and verify no results information", async () => { + //Search for not existing article by entering not findable string (special characters) + await HelpPage.searchForArticle("!@#$%^&*()") + + //Verify quick search: no results information + await expect(HelpPage.helpSearchResults).toBeDisplayed() + await expect(HelpPage.helpSearchResultsParagraph).toHaveText("We couldn't find any topics...") + } + ) +}) diff --git a/apps/mudita-center-e2e/src/specs/help/help-verify-feedback.ts b/apps/mudita-center-e2e/src/specs/help/help-verify-feedback.ts new file mode 100644 index 0000000000..d1205ddf72 --- /dev/null +++ b/apps/mudita-center-e2e/src/specs/help/help-verify-feedback.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import NavigationTabs from "../../page-objects/tabs.page" +import HelpArticlePage from "../../page-objects/help-article.page" +import HomePage from "../../page-objects/home.page" + +describe("Help - Verify Feedback", () => { + before(async () => { + const notNowButton = await HomePage.notNowButton + await notNowButton.waitForDisplayed() + await notNowButton.click() + }) + + it("Open Help window", async () => { + const helpTab = await NavigationTabs.helpTab + await helpTab.waitForDisplayed({ timeout: 15000 }) + await helpTab.click() + }) + + it("Check first article and give feedback as YES", async () => { + const firstArticle = HelpArticlePage.helpArticleItems[0] + await expect(firstArticle).toBeDisplayed() + await firstArticle.click() + + //Check article title + const helpArticleTitle = await HelpArticlePage.helpArticleTitle + await expect(helpArticleTitle).toHaveText( + "How to add music files to Harmony" + ) + + //Check article helpful section and vote YES + const helpArticleFeedbackYesButton = + await HelpArticlePage.helpArticleFeedbackYesButton + await expect(helpArticleFeedbackYesButton).toBeDisplayed() + await expect(helpArticleFeedbackYesButton).toBeClickable() + await helpArticleFeedbackYesButton.click() + + //Verify if "YES" vote was sent + await expect(helpArticleFeedbackYesButton).not.toBeDisplayed() + const iconNamaste = HelpArticlePage.iconNamaste + const feedbackThanksText = HelpArticlePage.feedbackThanksText + await expect(iconNamaste).toBeDisplayed() + await expect(feedbackThanksText).toBeDisplayed() + await expect(feedbackThanksText).toHaveText("Thank you for your opinion!") + + //Check back button and return from article + const helpArticleBackButton = await HelpArticlePage.helpArticleBackButton + await expect(helpArticleBackButton).toBeClickable() + helpArticleBackButton.click() + }) + + it("Check second article and give feedback as NO", async () => { + const secondArticle = HelpArticlePage.helpArticleItems[1] + await expect(secondArticle).toBeDisplayed() + await secondArticle.click() + + //Check article title + const helpArticleTitle = await HelpArticlePage.helpArticleTitle + await expect(helpArticleTitle).toHaveText( + "How to delete music files from Harmony" + ) + + //Check article helpful section and vote NO + const helpArticleFeedbackNoButton = + await HelpArticlePage.helpArticleFeedbackNoButton + await expect(helpArticleFeedbackNoButton).toBeDisplayed() + await expect(helpArticleFeedbackNoButton).toBeClickable() + await helpArticleFeedbackNoButton.click() + + //Verify if "NO" vote was sent + await expect(helpArticleFeedbackNoButton).not.toBeDisplayed() + const iconNamaste = HelpArticlePage.iconNamaste + const feedbackThanksText = HelpArticlePage.feedbackThanksText + await expect(iconNamaste).toBeDisplayed() + await expect(feedbackThanksText).toBeDisplayed() + await expect(feedbackThanksText).toHaveText("Thank you for your opinion!") + }) +}) diff --git a/apps/mudita-center-e2e/src/specs/help/help-window-check-offline.e2e.ts b/apps/mudita-center-e2e/src/specs/help/help-window-check-offline.e2e.ts deleted file mode 100644 index 6be150efc8..0000000000 --- a/apps/mudita-center-e2e/src/specs/help/help-window-check-offline.e2e.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import NavigationTabs from "../../page-objects/tabs.page" -import HelpPage from "../../page-objects/help.page" -import HomePage from "../../page-objects/home.page" -import HelpModalPage from "../../page-objects/help-modal.page" -import NewsPage from "../../page-objects/news.page" -import dns from "node:dns" - -describe("Check Help window in offline mode", () => { - before(async function () { - dns.setDefaultResultOrder("ipv4first") - await browser.throttle("offline") - - const notNowButton = await HomePage.notNowButton - await notNowButton.waitForDisplayed() - await notNowButton.click() - }) - - it("Open Help window", async () => { - const helpTab = await NavigationTabs.helpTab - await helpTab.waitForDisplayed({ timeout: 15000 }) - await helpTab.click() - - await browser.switchWindow("#/help") - - // Check window title - const helpTitle = await HelpPage.windowTitle - await helpTitle.waitForDisplayed({ timeout: 15000 }) - await expect(helpTitle).toHaveText("Mudita Center Help") - }) - - it("Check contents of Mudita Help", async () => { - // Check presence of the search engine - const searchIcon = await HelpPage.searchIcon - await expect(searchIcon).toBeDisplayed() - - const searchPlaceholder = await HelpPage.searchPlaceholder - await expect(searchPlaceholder).toHaveAttributeContaining( - "placeholder", - "Search" - ) - - // Check presence of Contact support button - const contactSupportButton = await HelpPage.contactSupportButton - await expect(contactSupportButton).toBeDisplayed() - await expect(contactSupportButton).toBeClickable() - - const contactSupportButtonTooltip = - await HelpPage.contactSupportButtonTooltip - contactSupportButton.moveTo() - await expect(contactSupportButtonTooltip).toBeDisplayed() - await expect(contactSupportButtonTooltip).toHaveText("Contact support") - - // Check accordion - const helpTopic = await HelpPage.listElement - - await expect(helpTopic).toBeDisplayed() - const noOfArticles = await HelpPage.listElements - await expect(noOfArticles).toBeElementsArrayOfSize({ gte: 25 }) - }) - - it("Check content of first article", async () => { - const helpTopic = await HelpPage.listElement - await expect(helpTopic).toHaveText( - "How to import my iCloud contacts into Mudita Pure by using .vcf file?" - ) - await helpTopic.click() - const helpTopicContent = await HelpPage.topicContent - await expect(helpTopicContent).toBeDisplayed() - await expect(helpTopicContent).toHaveTextContaining( - "Click on the “Import” button." - ) - const backLink = await HelpPage.articleBackLink - await backLink.click() - const helpTitle = await HelpPage.windowTitle - await expect(helpTitle).toHaveText("Mudita Center Help") - }) - - it("Search for questions & check search results", async () => { - const searchPlaceholder = await HelpPage.searchPlaceholder - searchPlaceholder.setValue("fail") - browser.keys("\uE007") - const helpTopic = await HelpPage.listElement - await expect(helpTopic).toHaveText("OS update failed") - searchPlaceholder.setValue("harMony") - await helpTopic.waitForDisplayed({ timeout: 15000 }) - await expect(helpTopic).toHaveText( - "How to connect my Mudita Harmony to Center?" - ) - const noOfArticles = await HelpPage.listElements - await expect(noOfArticles).toBeElementsArrayOfSize({ gte: 4 }) - }) - - it("Check Contact support modal", async () => { - const contactSupportButton = await HelpPage.contactSupportButton - await contactSupportButton.click() - const modalHeader = await HelpModalPage.modalHeader - await expect(modalHeader).toBeDisplayed - const closeButton = await HelpModalPage.closeModalButton - await closeButton.click() - - await browser.switchWindow("#/news") - const newsHeader = await NewsPage.newsHeader - await expect(newsHeader).toBeDisplayed - await expect(newsHeader).toHaveText("Mudita News") - }) -}) diff --git a/apps/mudita-center-e2e/src/specs/help/help-window-check.e2e.ts b/apps/mudita-center-e2e/src/specs/help/help-window-check.e2e.ts deleted file mode 100644 index a05bd5a71e..0000000000 --- a/apps/mudita-center-e2e/src/specs/help/help-window-check.e2e.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import NavigationTabs from "../../page-objects/tabs.page" -import HelpPage from "../../page-objects/help.page" -import HomePage from "../../page-objects/home.page" -import HelpModalPage from "../../page-objects/help-modal.page" -import NewsPage from "../../page-objects/news.page" - -describe("Check Help window", () => { - before(async () => { - const notNowButton = await HomePage.notNowButton - await notNowButton.waitForDisplayed() - await notNowButton.click() - }) - - it("Open Help window", async () => { - const helpTab = await NavigationTabs.helpTab - await helpTab.waitForDisplayed({ timeout: 15000 }) - await helpTab.click() - - await browser.switchWindow("#/help") - - // Check window title - const helpTitle = await HelpPage.windowTitle - await helpTitle.waitForDisplayed({ timeout: 15000 }) - await expect(helpTitle).toHaveText("Mudita Center Help") - }) - - it("Check contents of Mudita Help", async () => { - // Check presence of the search engine - const searchIcon = await HelpPage.searchIcon - await expect(searchIcon).toBeDisplayed() - - const searchPlaceholder = await HelpPage.searchPlaceholder - await expect(searchPlaceholder).toHaveAttributeContaining( - "placeholder", - "Search" - ) - - // Check presence of Contact support button - const contactSupportButton = await HelpPage.contactSupportButton - await expect(contactSupportButton).toBeDisplayed() - await expect(contactSupportButton).toBeClickable() - - const contactSupportButtonTooltip = - await HelpPage.contactSupportButtonTooltip - contactSupportButton.moveTo() - await expect(contactSupportButtonTooltip).toBeDisplayed() - await expect(contactSupportButtonTooltip).toHaveText("Contact support") - - // Check accordion - const helpTopic = await HelpPage.listElement - - await expect(helpTopic).toBeDisplayed() - const noOfArticles = await HelpPage.listElements - await expect(noOfArticles).toBeElementsArrayOfSize({ gte: 25 }) - }) - - it("Check content of first article", async () => { - const helpTopic = await HelpPage.listElement - await expect(helpTopic).toHaveText( - "How to import my iCloud contacts into Mudita Pure by using .vcf file?" - ) - await helpTopic.click() - const helpTopicContent = await HelpPage.topicContent - await expect(helpTopicContent).toBeDisplayed() - await expect(helpTopicContent).toHaveTextContaining( - "Click on the “Import” button." - ) - const backLink = await HelpPage.articleBackLink - await backLink.click() - const helpTitle = await HelpPage.windowTitle - await expect(helpTitle).toHaveText("Mudita Center Help") - }) - - it("Search for questions & check search results", async () => { - const searchPlaceholder = await HelpPage.searchPlaceholder - searchPlaceholder.setValue("fail") - browser.keys("\uE007") - const helpTopic = await HelpPage.listElement - await expect(helpTopic).toHaveText("OS update failed") - searchPlaceholder.setValue("harMony") - await helpTopic.waitForDisplayed({ timeout: 15000 }) - await expect(helpTopic).toHaveText( - "How to connect my Mudita Harmony to Center?" - ) - const noOfArticles = await HelpPage.listElements - await expect(noOfArticles).toBeElementsArrayOfSize({ gte: 4 }) - }) - - it("Check Contact support modal", async () => { - const contactSupportButton = await HelpPage.contactSupportButton - await contactSupportButton.click() - const modalHeader = await HelpModalPage.modalHeader - await expect(modalHeader).toBeDisplayed - const closeButton = await HelpModalPage.closeModalButton - await closeButton.click() - - await browser.switchWindow("#/news") - const newsHeader = await NewsPage.newsHeader - await expect(newsHeader).toBeDisplayed - await expect(newsHeader).toHaveText("Mudita News") - }) -}) diff --git a/apps/mudita-center-e2e/src/specs/news/more-news.e2e.ts b/apps/mudita-center-e2e/src/specs/news/more-news.e2e.ts new file mode 100644 index 0000000000..c24909d9c2 --- /dev/null +++ b/apps/mudita-center-e2e/src/specs/news/more-news.e2e.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import NavigationTabs from "../../page-objects/tabs.page" +import HomePage from "../../page-objects/home.page" +import NewsPage from "../../page-objects/news.page" + +describe("Check more news button", () => { + it("Click Not Now and Open Mudita News", async () => { + const notNowButton = await HomePage.notNowButton + await expect(notNowButton).toBeClickable() + await notNowButton.click() + const muditaNewsTab = NavigationTabs.muditaNewsTab + await expect(muditaNewsTab).toBeDisplayed() + }) + + it("Check Mudita News Header, check More News button href", async () => { + const newsHeader = NewsPage.newsHeader + await expect(newsHeader).toBeDisplayed() + await expect(newsHeader).toHaveText("Mudita News") + + const moreNewsButton = await NewsPage.moreNewsButton + await expect(moreNewsButton).toBeDisplayed() + + const moreNewsButtonHref = NewsPage.moreNewsButtonHref + const checkHref = await moreNewsButtonHref.getAttribute("href") + await expect(checkHref).toBe("https://www.mudita.com/#news") + }) +}) diff --git a/apps/mudita-center-e2e/src/specs/overview/kompakt-backup-getting-initial-info.ts b/apps/mudita-center-e2e/src/specs/overview/kompakt-backup-getting-initial-info.ts new file mode 100644 index 0000000000..f4ce089902 --- /dev/null +++ b/apps/mudita-center-e2e/src/specs/overview/kompakt-backup-getting-initial-info.ts @@ -0,0 +1,192 @@ +import { E2EMockClient } from "../../../../../libs/e2e-mock/client/src" +import { + overviewConfigForBackup, + overviewDataWithOneSimCard, +} from "../../../../../libs/e2e-mock/responses/src" +import ModalBackupKompaktPage from "../../page-objects/modal-backup-kompakt.page" + +describe("E2E mock sample - overview view", () => { + before(async () => { + E2EMockClient.connect() + //wait for a connection to be established + await browser.waitUntil(() => { + return E2EMockClient.checkConnection() + }) + }) + + after(() => { + E2EMockClient.stopServer() + E2EMockClient.disconnect() + }) + + it("Connect device", async () => { + E2EMockClient.mockResponse({ + path: "path-1", + body: overviewConfigForBackup, + endpoint: "FEATURE_CONFIGURATION", + method: "GET", + status: 200, + }) + E2EMockClient.mockResponse({ + path: "path-1", + body: overviewDataWithOneSimCard, + endpoint: "FEATURE_DATA", + method: "GET", + status: 200, + }) + E2EMockClient.addDevice({ + path: "path-1", + serialNumber: "first-serial-number", + }) + + await browser.pause(6000) + const menuItem = await $(`//a[@href="#/generic/mc-overview"]`) + + await menuItem.waitForDisplayed({ timeout: 10000 }) + await expect(menuItem).toBeDisplayed() + }) + + it("Wait for Overview Page and click Create Backup", async () => { + const createBackupButton = await ModalBackupKompaktPage.createBackupButton + await expect(createBackupButton).toBeDisplayed() + await expect(createBackupButton).toBeClickable() + await createBackupButton.click() + }) + + it("Verify modal in scope of available backup options, verify text and buttons", async () => { + const contactList = await ModalBackupKompaktPage.contactList + await expect(contactList).toBeDisplayed() + const contactListText = await contactList?.getProperty("textContent") + expect(contactListText).toContain("Contact list") + expect(contactListText).not.toContain("Coming soon!") + + const callLog = await ModalBackupKompaktPage.callLog + await expect(callLog).toBeDisplayed() + const callLogText = await callLog?.getProperty("textContent") + expect(callLogText).toContain("Call log") + expect(callLogText).not.toContain("Coming soon!") + + const backupModalTitle = ModalBackupKompaktPage.backupModalTitle + await expect(backupModalTitle).toBeDisplayed() + await expect(backupModalTitle).toHaveText("Create backup") + + const backupModalDescription = ModalBackupKompaktPage.backupModalDescription + await expect(backupModalDescription).toBeDisplayed() + await expect(backupModalDescription).toHaveText( + "All backup data stays on your computer." + ) + + const backupModalCancel = ModalBackupKompaktPage.backupModalCancel + await expect(backupModalCancel).toBeClickable() + + const backupModalClose = ModalBackupKompaktPage.backupModalClose + await expect(backupModalClose).toBeClickable() + }) + + it("Click Create backup - verify modal about create password for backup", async () => { + const createBackupProceedNext = + await ModalBackupKompaktPage.createBackupProceedNext + await expect(createBackupProceedNext).toBeClickable() + await createBackupProceedNext.click() + + const createBackupPasswordModalTitle = + ModalBackupKompaktPage.createBackupPasswordModalTitle + await expect(createBackupPasswordModalTitle).toHaveTextContaining( + "Create password for backup" + ) + + const createBackupPasswordOptionalText = + ModalBackupKompaktPage.createBackupPasswordOptionalText + await expect(createBackupPasswordOptionalText).toHaveText("(optional)") + + const createBackupPasswordModalDescription = + ModalBackupKompaktPage.createBackupPasswordModalDescription + await expect(createBackupPasswordModalDescription).toHaveTextContaining( + "You can protect backup with a new password." + ) + + const createBackupPasswordModalDescriptionMore = + ModalBackupKompaktPage.createBackupPasswordModalDescriptionMore + await expect(createBackupPasswordModalDescriptionMore).toHaveText( + "* You can't change/recover the password later." + ) + + const createBackupPasswordPlaceholder = + ModalBackupKompaktPage.createBackupPasswordPlaceholder + await expect(createBackupPasswordPlaceholder).toBeClickable() + + const createBackupPasswordRepeatPlaceholder = + ModalBackupKompaktPage.createBackupPasswordRepeatPlaceholder + await expect(createBackupPasswordRepeatPlaceholder).toBeClickable() + + const createBackupPasswordConfirm = + ModalBackupKompaktPage.createBackupPasswordConfirm + await expect(createBackupPasswordConfirm).not.toBeClickable() + + const createBackupPasswordSkip = + ModalBackupKompaktPage.createBackupPasswordSkip + await expect(createBackupPasswordSkip).toBeClickable() + + const createBackupPasswordClose = + ModalBackupKompaktPage.createBackupPasswordClose + await expect(createBackupPasswordClose).toBeClickable() + }) + + it("Fill password for a backup, unhide it and verify value and design", async () => { + const inputPassword = ModalBackupKompaktPage.inputPassword + await inputPassword.click() + const randomPassword = Math.random().toString(36).substring(2, 10) + await inputPassword.setValue(randomPassword) + + const checkPassword = await inputPassword.getAttribute("type") + await expect(checkPassword).toBe("password") + + const unhidePasswordIcon = ModalBackupKompaktPage.unhidePasswordIcon + await unhidePasswordIcon.click() + + //Verify design, and it's value to check if user can hide password if it was displayed + const hidePasswordIcon = ModalBackupKompaktPage.hidePasswordIcon + await expect(hidePasswordIcon).toBeClickable() + }) + + it("Fill repeat password for a backup, unhide it and verify value and design, verify (passwords do not match)", async () => { + const repeatInputPassword = ModalBackupKompaktPage.repeatInputPassword + await repeatInputPassword.click() + const randomPassword2 = Math.random().toString(36).substring(2, 10) + await repeatInputPassword.setValue(randomPassword2) + + const checkPassword = await repeatInputPassword.getAttribute("type") + await expect(checkPassword).toBe("password") + + const unhidePasswordIcon = ModalBackupKompaktPage.unhidePasswordIcon + await unhidePasswordIcon.click() + + //Verify design and it's value to check if user can hide password if it was displayed + const hidePasswordIcon = ModalBackupKompaktPage.hidePasswordIcon + await expect(hidePasswordIcon).toBeClickable() + + const passwordsDoNotMatch = ModalBackupKompaktPage.passwordsDoNotMatch + await expect(passwordsDoNotMatch).toHaveText("Passwords do not match") + }) + + it("Fill repeat password with first filed password and verify if passwords do not match is gone", async () => { + const inputPassword = ModalBackupKompaktPage.inputPassword + const repeatInputPassword = ModalBackupKompaktPage.repeatInputPassword + const randomPassword = Math.random().toString(36).substring(2, 10) + await inputPassword.click() + await inputPassword.clearValue() + await inputPassword.setValue(randomPassword) + + await repeatInputPassword.click() + await repeatInputPassword.clearValue() + await repeatInputPassword.setValue(randomPassword) + + const passwordsDoNotMatch = ModalBackupKompaktPage.passwordsDoNotMatch + await expect(passwordsDoNotMatch).not.toBeDisplayed() + + const createBackupPasswordConfirm = + ModalBackupKompaktPage.createBackupPasswordConfirm + await expect(createBackupPasswordConfirm).toBeClickable() + await createBackupPasswordConfirm.click() + }) +}) diff --git a/apps/mudita-center-e2e/src/specs/stress-tests/connected-devices-stress-test.ts b/apps/mudita-center-e2e/src/specs/stress-tests/connected-devices-stress-test.ts index 8fdb23453c..4d2c6b8e90 100644 --- a/apps/mudita-center-e2e/src/specs/stress-tests/connected-devices-stress-test.ts +++ b/apps/mudita-center-e2e/src/specs/stress-tests/connected-devices-stress-test.ts @@ -70,8 +70,11 @@ describe("Kompakt switching devices", () => { "Select a device to continue" ) - const availableDevices = selectDevicePage.availableDevices - await expect(availableDevices).toBeDisplayed() + const availableDevices = await selectDevicePage.availableDevices + + for (const device of availableDevices) { + await expect(device).toBeDisplayed(); + } const firstDeviceOnSelectModal = await selectDevicePage.getDeviceOnSelectModal(1) diff --git a/apps/mudita-center-e2e/src/specs/stress-tests/device-drawer-stress-test.ts b/apps/mudita-center-e2e/src/specs/stress-tests/device-drawer-stress-test.ts index 0c6c5a17cb..c688cd46b1 100644 --- a/apps/mudita-center-e2e/src/specs/stress-tests/device-drawer-stress-test.ts +++ b/apps/mudita-center-e2e/src/specs/stress-tests/device-drawer-stress-test.ts @@ -46,7 +46,8 @@ describe("Kompakt switching devices", () => { }, ] - for (const device of devices) { + for (let i = 0; i < devices.length; i++) { + const device = devices[i] E2EMockClient.mockResponse({ path: device.path, body: device.body, @@ -59,7 +60,12 @@ describe("Kompakt switching devices", () => { serialNumber: device.serialNumber, }) - await browser.pause(6000) + // Add a 4-second wait after adding the first device + if (i === 0) { + await browser.pause(10000) + } else { + await browser.pause(6000) + } } }) diff --git a/apps/mudita-center-e2e/src/test-filenames/consts/test-filenames.const.ts b/apps/mudita-center-e2e/src/test-filenames/consts/test-filenames.const.ts index 0ab7dc591e..c94a52db37 100644 --- a/apps/mudita-center-e2e/src/test-filenames/consts/test-filenames.const.ts +++ b/apps/mudita-center-e2e/src/test-filenames/consts/test-filenames.const.ts @@ -6,18 +6,19 @@ // the format should stay as it is - it should contain `./` at the beginning export enum TestFilesPaths { messagesInAppNavigationTest = "src/specs/messages/messages-in-app-navigation.e2e.ts", - helpWindowCheckTest = "src/specs/help/help-window-check.e2e.ts", + helpSectionCheckTest = "src/specs/help/help-section-check.e2e.ts", + helpSectionCheckTestOffline = "src/specs/help/help-section-check-offline.e2e.ts", homePageTestDeviceNotConnectedTest = "src/specs/overview/home-page-device-not-connecting.e2e.ts", e2eMockSample = "src/specs/overview/e2e-mock-sample.e2e.ts", mcCheckForUpdatesTest = "src/specs/settings/mc-version-check-for-updates.e2e.ts", newsPageOnlineTest = "src/specs/news/news-check-online.e2e.ts", + newsMoreNews = "src/specs/news/more-news.e2e.ts", newsPageOfflineTest = "src/specs/news/news-check-offline.e2e.ts", termsOfServiceTest = "src/specs/settings/terms-of-service.e2e.ts", backupLocationTest = "src/specs/settings/backup-location.e2e.ts", mcCheckForUpdatesOfflineTest = "src/specs/settings/mc-version-check-for-updates-offline.e2e.ts", privacyPolicyTest = "src/specs/settings/privacy-policy.e2e.ts", licenseTest = "src/specs/settings/license.e2e.ts", - helpWindowCheckOfflineTest = "src/specs/help/help-window-check-offline.e2e.ts", mcHomePageForceUpdateTest = "src/specs/overview/e2e-mock-mc-force-update-available.e2e.ts", mcHomePageForceUpdateErrorTest = "src/specs/overview/e2e-mock-mc-force-update-error.e2e.ts", mcHomePageSoftUpdateTest = "src/specs/overview/e2e-mock-mc-soft-update-available.e2e.ts", @@ -27,5 +28,10 @@ export enum TestFilesPaths { kompaktAbout = "src/specs/overview/kompakt-about.ts", kompaktConnectedDevicesModalStressTest = "src/specs/stress-tests/connected-devices-stress-test.ts", kompaktDrawerStressTest = "src/specs/stress-tests/device-drawer-stress-test.ts", + contactSupportUnhappyPath = "src/specs/help/contact-support-unhappy-path.ts", + kompaktBackupModalGettingInitialInfo = "src/specs/overview/kompakt-backup-getting-initial-info.ts", + helpVerifyFeedback = "src/specs/help/help-verify-feedback.ts", + helpSectionSearchNoResults = "src/specs/help/help-section-search-noresults.e2e.ts", + helpLinkInsideContainer = "src/specs/help/help-link-inside-container.ts", } export const toRelativePath = (path: string) => `./${path}` diff --git a/apps/mudita-center-e2e/tsconfig.json b/apps/mudita-center-e2e/tsconfig.json index aae4540887..eb7f7b6494 100644 --- a/apps/mudita-center-e2e/tsconfig.json +++ b/apps/mudita-center-e2e/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "types": [ "node", - "webdriverio/async", + "@wdio/globals/types", "@wdio/mocha-framework", "expect-webdriverio" ], diff --git a/apps/mudita-center-e2e/wdio.conf.js b/apps/mudita-center-e2e/wdio.conf.js deleted file mode 100644 index 6f1e461bb9..0000000000 --- a/apps/mudita-center-e2e/wdio.conf.js +++ /dev/null @@ -1,346 +0,0 @@ -"use strict"; -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.config = void 0; -var dotenv = require("dotenv"); -var test_filenames_1 = require("./src/test-filenames"); -dotenv.config(); -exports.config = { - // - // ==================== - // Runner Configuration - // ==================== - // - // - // ===================== - // ts-node Configurations - // ===================== - // - // You can write tests using TypeScript to get autocompletion and type safety. - // You will need typescript and ts-node installed as devDependencies. - // WebdriverIO will automatically detect if these dependencies are installed - // and will compile your config and tests for you. - // If you need to configure how ts-node runs please use the - // environment variables for ts-node or use wdio config's autoCompileOpts section. - // - autoCompileOpts: { - autoCompile: true, - // see https://github.com/TypeStrong/ts-node#cli-and-programmatic-options - // for all available options - tsNodeOpts: { - transpileOnly: true, - project: "tsconfig.json", - }, - }, - // - // ================== - // Specify Test Files - // ================== - // Define which test specs should run. The pattern is relative to the directory - // from which `wdio` was called. - // - // The specs are defined as an array of spec files (optionally using wildcards - // that will be expanded). The test for each spec file will be run in a separate - // worker process. In order to have a group of spec files run in the same worker - // process simply enclose them in an array within the specs array. - // - // If you are calling `wdio` from an NPM script (see https://docs.npmjs.com/cli/run-script), - // then the current working directory is where your `package.json` resides, so `wdio` - // will be called from there. - // - specs: [ - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.messagesInAppNavigationTest), - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.helpWindowCheckTest), - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.mcCheckForUpdatesTest), - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.homePageTestDeviceNotConnectedTest) - ], - suites: { - standalone: [ - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.helpWindowCheckTest), - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.mcCheckForUpdatesTest), - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.homePageTestDeviceNotConnectedTest) - ], - multidevicePureHarmony: [], - multideviceSingleHarmony: [], - multideviceSinglePure: [], - multideviceSingleKompakt: [], - multidevicePureKompakt: [], - multideviceHarmonyKompakt: [], - multideviceGeneral: [], - harmony: [], - pure: [ - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.messagesInAppNavigationTest), - ], - kompakt: [], - deviceUpdate: [], - cicd: [ - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.helpWindowCheckTest), - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.mcCheckForUpdatesTest), - (0, test_filenames_1.toRelativePath)(test_filenames_1.TestFilesPaths.homePageTestDeviceNotConnectedTest) - ], - }, - // Patterns to exclude. - exclude: [ - // 'path/to/excluded/files' - ], - filesToWatch: ["./src/specs/**/*.e2e.ts"], - // - // ============ - // Capabilities - // ============ - // Define your capabilities here. WebdriverIO can run multiple capabilities at the same - // time. Depending on the number of capabilities, WebdriverIO launches several test - // sessions. Within your capabilities you can overwrite the spec and exclude options in - // order to group specific specs to a specific capability. - // - // First, you can define how many instances should be started at the same time. Let's - // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have - // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec - // files and you set maxInstances to 10, all spec files will get tested at the same time - // and 30 processes will get spawned. The property handles how many capabilities - // from the same test should run tests. - // - maxInstances: 1, - // - // If you have trouble getting all important capabilities together, check out the - // Sauce Labs platform configurator - a great tool to configure your capabilities: - // https://saucelabs.com/platform/platform-configurator - // - capabilities: [ - { - // maxInstances can get overwritten per capability. So if you have an in-house Selenium - // grid with only 5 firefox instances available you can make sure that not more than - // 5 instances get started at a time. - // maxInstances: 5, - // - browserName: "chrome", - "goog:chromeOptions": { - binary: process.env.TEST_BINARY_PATH, - args: [], - }, - // If outputDir is provided WebdriverIO can capture driver session logs - // it is possible to configure which logTypes to include/exclude. - // excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs - // excludeDriverLogs: ['bugreport', 'server'], - }, - ], - // - // =================== - // Test Configurations - // =================== - // Define all options that are relevant for the WebdriverIO instance here - // - // Level of logging verbosity: trace | debug | info | warn | error | silent - logLevel: process.env.TEST_LOG_LEVEL || "info", - // - // Set specific log levels per logger - // loggers: - // - webdriver, webdriverio - // - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service - // - @wdio/mocha-framework, @wdio/jasmine-framework - // - @wdio/local-runner - // - @wdio/sumologic-reporter - // - @wdio/cli, @wdio/config, @wdio/utils - // Level of logging verbosity: trace | debug | info | warn | error | silent - // logLevels: { - // webdriver: 'info', - // '@wdio/appium-service': 'info' - // }, - // - // If you only want to run your tests until a specific amount of tests have failed use - // bail (default is 0 - don't bail, run all tests). - bail: 0, - // - // Set a base URL in order to shorten url command calls. If your `url` parameter starts - // with `/`, the base url gets prepended, not including the path portion of your baseUrl. - // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url - // gets prepended directly. - baseUrl: "http://localhost", - // - // Default timeout for all waitFor* commands. - waitforTimeout: 6000, - // - // Default timeout in milliseconds for request - // if browser driver or grid doesn't send response - connectionRetryTimeout: 120000, - // - // Default request retries count - connectionRetryCount: 3, - // - // Test runner services - // Services take over a specific job you don't want to take care of. They enhance - // your test setup with almost no effort. Unlike plugins, they don't add new - // commands. Instead, they hook themselves up into the test process. - services: ["chromedriver"], - // Framework you want to run your specs with. - // The following are supported: Mocha, Jasmine, and Cucumber - // see also: https://webdriver.io/docs/frameworks - // - // Make sure you have the wdio adapter package for the specific framework installed - // before running any tests. - framework: "mocha", - // - // The number of times to retry the entire specfile when it fails as a whole - // specFileRetries: 1, - // - // Delay in seconds between the spec file retry attempts - // specFileRetriesDelay: 0, - // - // Whether or not retried specfiles should be retried immediately or deferred to the end of the queue - // specFileRetriesDeferred: false, - // - // Test reporter for stdout. - // The only one supported by default is 'dot' - // see also: https://webdriver.io/docs/dot-reporter - reporters: [ - "spec", - [ - "json", - { - outputDir: "./results", - }, - ], - ], - // - // Options to be passed to Mocha. - // See the full list at http://mochajs.org/ - mochaOpts: { - ui: "bdd", - timeout: 60000000, - }, - // - // ===== - // Hooks - // ===== - // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance - // it and to build services around it. You can either apply a single function or an array of - // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got - // resolved to continue. - /** - * Gets executed once before all workers get launched. - * @param {Object} config wdio configuration object - * @param {Array.} capabilities list of capabilities details - */ - // onPrepare: async function (_config, _capabilities) {}, - /** - * Gets executed before a worker process is spawned and can be used to initialise specific service - * for that worker as well as modify runtime environments in an async fashion. - * @param {String} cid capability id (e.g 0-0) - * @param {[type]} caps object containing capabilities for session that will be spawn in the worker - * @param {[type]} specs specs to be run in the worker process - * @param {[type]} args object that will be merged with the main configuration once worker is initialised - * @param {[type]} execArgv list of string arguments passed to the worker process - */ - // onWorkerStart: function (cid, caps, specs, args, execArgv) {}, - /** - * Gets executed just before initialising the webdriver session and test framework. It allows you - * to manipulate configurations depending on the capability or spec. - * @param {Object} config wdio configuration object - * @param {Array.} capabilities list of capabilities details - * @param {Array.} specs List of spec file paths that are to be run - * @param {String} cid worker id (e.g. 0-0) - */ - // beforeSession: async function (_config, _capabilities, specs, _cid) {}, - /** - * Gets executed before test execution begins. At this point you can access to all global - * variables like `browser`. It is the perfect place to define custom commands. - * @param {Array.} capabilities list of capabilities details - * @param {Array.} specs List of spec file paths that are to be run - * @param {Object} browser instance of created browser/device session - */ - // before: function (capabilities, specs) { - // }, - /** - * Runs before a WebdriverIO command gets executed. - * @param {String} commandName hook command name - * @param {Array} args arguments that command would receive - */ - // beforeCommand: function (commandName, args) { - // }, - /** - * Hook that gets executed before the suite starts - * @param {Object} suite suite details - */ - // beforeSuite: function (suite) { - // }, - /** - * Function to be executed before a test (in Mocha/Jasmine) starts. - */ - // beforeTest: function (test, context) { - // }, - /** - * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling - * beforeEach in Mocha) - */ - // beforeHook: function (test, context) { - // }, - /** - * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling - * afterEach in Mocha) - */ - // afterHook: function (test, context, { error, result, duration, passed, retries }) { - // }, - /** - * Function to be executed after a test (in Mocha/Jasmine only) - * @param {Object} test test object - * @param {Object} context scope object the test was executed with - * @param {Error} result.error error object in case the test fails, otherwise `undefined` - * @param {Any} result.result return object of test function - * @param {Number} result.duration duration of test - * @param {Boolean} result.passed true if test has passed, otherwise false - * @param {Object} result.retries informations to spec related retries, e.g. `{ attempts: 0, limit: 0 }` - */ - // afterTest: function(test, context, { error, result, duration, passed, retries }) { - // }, - /** - * Hook that gets executed after the suite has ended - * @param {Object} suite suite details - */ - // afterSuite: function (suite) { - // }, - /** - * Runs after a WebdriverIO command gets executed - * @param {String} commandName hook command name - * @param {Array} args arguments that command would receive - * @param {Number} result 0 - command success, 1 - command error - * @param {Object} error error object if any - */ - // afterCommand: function (commandName, args, result, error) { - // }, - /** - * Gets executed after all tests are done. You still have access to all global variables from - * the test. - * @param {Number} result 0 - test pass, 1 - test fail - * @param {Array.} capabilities list of capabilities details - * @param {Array.} specs List of spec file paths that ran - */ - // after: function (result, capabilities, specs) { - // }, - /** - * Gets executed right after terminating the webdriver session. - * @param {Object} config wdio configuration object - * @param {Array.} capabilities list of capabilities details - * @param {Array.} specs List of spec file paths that ran - */ - // afterSession: async function (_config, _capabilities, _specs) {}, - /** - * Gets executed after all workers got shut down and the process is about to exit. An error - * thrown in the onComplete hook will result in the test run failing. - * @param {Object} exitCode 0 - success, 1 - fail - * @param {Object} config wdio configuration object - * @param {Array.} capabilities list of capabilities details - * @param {} results object containing test results - */ - // onComplete: function(exitCode, config, capabilities, results) { - // }, - /** - * Gets executed when a refresh happens. - * @param {String} oldSessionId session ID of the old session - * @param {String} newSessionId session ID of the new session - */ - //onReload: function(oldSessionId, newSessionId) { - //} -}; diff --git a/apps/mudita-center-e2e/wdio.conf.ts b/apps/mudita-center-e2e/wdio.conf.ts index 2da0d93030..ad56e458b2 100644 --- a/apps/mudita-center-e2e/wdio.conf.ts +++ b/apps/mudita-center-e2e/wdio.conf.ts @@ -4,6 +4,7 @@ */ import type { Options } from "@wdio/types" +import path from "path" import * as dotenv from "dotenv" import { TestFilesPaths, toRelativePath } from "./src/test-filenames" @@ -55,17 +56,18 @@ export const config: Options.Testrunner = { // specs: [ toRelativePath(TestFilesPaths.messagesInAppNavigationTest), - toRelativePath(TestFilesPaths.helpWindowCheckTest), + toRelativePath(TestFilesPaths.helpSectionCheckTest), + toRelativePath(TestFilesPaths.helpSectionCheckTestOffline), toRelativePath(TestFilesPaths.mcCheckForUpdatesTest), toRelativePath(TestFilesPaths.homePageTestDeviceNotConnectedTest), toRelativePath(TestFilesPaths.newsPageOnlineTest), toRelativePath(TestFilesPaths.newsPageOfflineTest), + toRelativePath(TestFilesPaths.newsMoreNews), toRelativePath(TestFilesPaths.termsOfServiceTest), toRelativePath(TestFilesPaths.backupLocationTest), toRelativePath(TestFilesPaths.mcCheckForUpdatesOfflineTest), toRelativePath(TestFilesPaths.privacyPolicyTest), toRelativePath(TestFilesPaths.licenseTest), - toRelativePath(TestFilesPaths.helpWindowCheckOfflineTest), toRelativePath(TestFilesPaths.kompaktOverview), toRelativePath(TestFilesPaths.kompaktSwitchingDevices), toRelativePath(TestFilesPaths.mcHomePageForceUpdateTest), @@ -75,22 +77,32 @@ export const config: Options.Testrunner = { toRelativePath(TestFilesPaths.kompaktAbout), toRelativePath(TestFilesPaths.kompaktConnectedDevicesModalStressTest), toRelativePath(TestFilesPaths.kompaktDrawerStressTest), + toRelativePath(TestFilesPaths.contactSupportUnhappyPath), + toRelativePath(TestFilesPaths.kompaktBackupModalGettingInitialInfo), + toRelativePath(TestFilesPaths.helpVerifyFeedback), + toRelativePath(TestFilesPaths.helpSectionSearchNoResults), + toRelativePath(TestFilesPaths.helpLinkInsideContainer), ], suites: { standalone: [ - //toRelativePath(TestFilesPaths.helpWindowCheckTest), + toRelativePath(TestFilesPaths.helpSectionCheckTest), + toRelativePath(TestFilesPaths.helpSectionCheckTestOffline), //toRelativePath(TestFilesPaths.homePageTestDeviceNotConnectedTest), toRelativePath(TestFilesPaths.newsPageOnlineTest), + toRelativePath(TestFilesPaths.newsMoreNews), toRelativePath(TestFilesPaths.termsOfServiceTest), toRelativePath(TestFilesPaths.backupLocationTest), toRelativePath(TestFilesPaths.privacyPolicyTest), toRelativePath(TestFilesPaths.licenseTest), + toRelativePath(TestFilesPaths.contactSupportUnhappyPath), + toRelativePath(TestFilesPaths.helpVerifyFeedback), + toRelativePath(TestFilesPaths.helpSectionSearchNoResults), + toRelativePath(TestFilesPaths.helpLinkInsideContainer), ], mock: [ toRelativePath(TestFilesPaths.mcCheckForUpdatesTest), toRelativePath(TestFilesPaths.mcCheckForUpdatesOfflineTest), toRelativePath(TestFilesPaths.newsPageOfflineTest), - //toRelativePath(TestFilesPaths.helpWindowCheckOfflineTest), toRelativePath(TestFilesPaths.mcHomePageSoftUpdateTest), toRelativePath(TestFilesPaths.mcHomePageSoftUpdateErrorTest), toRelativePath(TestFilesPaths.mcHomePageForceUpdateTest), @@ -100,6 +112,7 @@ export const config: Options.Testrunner = { toRelativePath(TestFilesPaths.kompaktAbout), toRelativePath(TestFilesPaths.kompaktConnectedDevicesModalStressTest), toRelativePath(TestFilesPaths.kompaktDrawerStressTest), + toRelativePath(TestFilesPaths.kompaktBackupModalGettingInitialInfo), ], multidevicePureHarmony: [], multideviceSingleHarmony: [], @@ -113,16 +126,22 @@ export const config: Options.Testrunner = { kompakt: [], deviceUpdate: [], cicdStandalone: [ - //toRelativePath(TestFilesPaths.helpWindowCheckTest), + toRelativePath(TestFilesPaths.helpSectionCheckTest), + toRelativePath(TestFilesPaths.helpSectionCheckTestOffline), //toRelativePath(TestFilesPaths.homePageTestDeviceNotConnectedTest), toRelativePath(TestFilesPaths.newsPageOnlineTest), toRelativePath(TestFilesPaths.termsOfServiceTest), + toRelativePath(TestFilesPaths.contactSupportUnhappyPath), + toRelativePath(TestFilesPaths.newsMoreNews), + toRelativePath(TestFilesPaths.helpVerifyFeedback), + toRelativePath(TestFilesPaths.helpSectionSearchNoResults), + toRelativePath(TestFilesPaths.helpLinkInsideContainer), ], cicdMock: [ + toRelativePath(TestFilesPaths.contactSupportUnhappyPath), toRelativePath(TestFilesPaths.mcCheckForUpdatesTest), toRelativePath(TestFilesPaths.mcCheckForUpdatesOfflineTest), toRelativePath(TestFilesPaths.newsPageOfflineTest), - //toRelativePath(TestFilesPaths.helpWindowCheckOfflineTest), toRelativePath(TestFilesPaths.mcHomePageForceUpdateTest), toRelativePath(TestFilesPaths.mcHomePageForceUpdateErrorTest), toRelativePath(TestFilesPaths.mcHomePageSoftUpdateTest), @@ -132,6 +151,7 @@ export const config: Options.Testrunner = { toRelativePath(TestFilesPaths.kompaktAbout), toRelativePath(TestFilesPaths.kompaktConnectedDevicesModalStressTest), toRelativePath(TestFilesPaths.kompaktDrawerStressTest), + toRelativePath(TestFilesPaths.kompaktBackupModalGettingInitialInfo), ], }, // Patterns to exclude. @@ -173,6 +193,17 @@ export const config: Options.Testrunner = { binary: process.env.TEST_BINARY_PATH, args: [], }, + "wdio:chromedriverOptions": { + binary: path.resolve( + __dirname, + "..", + "..", + "node_modules", + "chromedriver", + "bin", + "chromedriver" + ), + }, // If outputDir is provided WebdriverIO can capture driver session logs // it is possible to configure which logTypes to include/exclude. // excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs diff --git a/apps/mudita-center/package-lock.json b/apps/mudita-center/package-lock.json index c6474fb501..b12f9e0fbe 100644 --- a/apps/mudita-center/package-lock.json +++ b/apps/mudita-center/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mudita/mudita-center-app", - "version": "2.4.0", + "version": "2.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mudita/mudita-center-app", - "version": "2.4.0", + "version": "2.5.0", "license": "GPL-3.0", "dependencies": { "serialport": "10.1.0" diff --git a/apps/mudita-center/package.json b/apps/mudita-center/package.json index c7a5e560e6..7c393dc3d1 100644 --- a/apps/mudita-center/package.json +++ b/apps/mudita-center/package.json @@ -1,6 +1,6 @@ { "name": "mudita-center", - "version": "2.4.0", + "version": "2.5.0", "description": "Mudita Center", "main": "./dist/main.js", "productName": "Mudita Center", diff --git a/apps/mudita-center/src/app.tsx b/apps/mudita-center/src/app.tsx index e3c5a2b39f..cab83f0456 100644 --- a/apps/mudita-center/src/app.tsx +++ b/apps/mudita-center/src/app.tsx @@ -3,6 +3,7 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +import "./wdyr" import "reflect-metadata" import translationConfig from "App/translations.config.json" import App from "App/app.component" diff --git a/apps/mudita-center/src/wdyr.ts b/apps/mudita-center/src/wdyr.ts new file mode 100644 index 0000000000..bcd74bcdf4 --- /dev/null +++ b/apps/mudita-center/src/wdyr.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React from "react" + +if (process.env.NODE_ENV === "development") { + const whyDidYouRender = require("@welldone-software/why-did-you-render") + whyDidYouRender(React, { + trackAllPureComponents: false, + }) +} diff --git a/libs/core-device/models/src/endpoint.constant.ts b/libs/core-device/models/src/endpoint.constant.ts index f1a4f6fc76..c69633650e 100644 --- a/libs/core-device/models/src/endpoint.constant.ts +++ b/libs/core-device/models/src/endpoint.constant.ts @@ -16,6 +16,7 @@ export enum Endpoint { CallLog = 9, Security = 13, Outbox = 14, + TimeSynchronization = 16, // api version (mocked) ApiVersion = 1000, diff --git a/libs/core/__deprecated__/api/mudita-center-server/mudita-center-server-routes.ts b/libs/core/__deprecated__/api/mudita-center-server/mudita-center-server-routes.ts index 7e7aa5189a..6e845e328a 100644 --- a/libs/core/__deprecated__/api/mudita-center-server/mudita-center-server-routes.ts +++ b/libs/core/__deprecated__/api/mudita-center-server/mudita-center-server-routes.ts @@ -8,4 +8,5 @@ export enum MuditaCenterServerRoutes { GetReleaseV2 = "v2-get-release", AppConfigurationV2 = "v2-app-configuration", ExternalUsageDevice = "external-usage-device", + GetMscFlashDetails = "msc-flash", } diff --git a/libs/core/__deprecated__/renderer/components/core/icon/icon-type.ts b/libs/core/__deprecated__/renderer/components/core/icon/icon-type.ts index 0ff97b9c54..4def9c3805 100644 --- a/libs/core/__deprecated__/renderer/components/core/icon/icon-type.ts +++ b/libs/core/__deprecated__/renderer/components/core/icon/icon-type.ts @@ -8,6 +8,7 @@ export enum IconType { ArrowLongLeft, ArrowLongRight, AttachContact, + BackArrowIcon, BackupFolder, Battery, BorderCheckIcon, @@ -16,6 +17,7 @@ export enum IconType { ChargingBattery, Check, CheckCircle, + CheckCircleBlack, CheckIndeterminate, Cloud, Close, @@ -130,4 +132,6 @@ export enum IconType { DataMigration, RecoveryModeWhite, RecoveryModeBlack, + ButtonSuccess, + LightButton, } diff --git a/libs/core/__deprecated__/renderer/components/core/icon/icon.config.ts b/libs/core/__deprecated__/renderer/components/core/icon/icon.config.ts index 495c2f9a15..0ab1afadad 100644 --- a/libs/core/__deprecated__/renderer/components/core/icon/icon.config.ts +++ b/libs/core/__deprecated__/renderer/components/core/icon/icon.config.ts @@ -8,12 +8,14 @@ import Arrow from "Core/__deprecated__/renderer/svg/arrow.svg" import ArrowLongLeft from "Core/__deprecated__/renderer/svg/arrow-long-left.svg" import ArrowLongRight from "Core/__deprecated__/renderer/svg/arrow-long-right.svg" import AttachContact from "Core/__deprecated__/renderer/svg/attach-contact.svg" +import BackArrowIcon from "Core/__deprecated__/renderer/svg/back-arrow-icon.svg" import BackupFolder from "Core/__deprecated__/renderer/svg/backup-folder.svg" import Battery from "Core/__deprecated__/renderer/svg/battery.svg" import BorderCheck from "Core/__deprecated__/renderer/svg/border-check-icon.svg" import ChargingBattery from "Core/__deprecated__/renderer/svg/charging-battery.svg" import Check from "Core/__deprecated__/renderer/svg/check-icon.svg" import CheckCircle from "Core/__deprecated__/renderer/svg/check-circle.svg" +import CheckCircleBlack from "Core/__deprecated__/renderer/svg/check-circle-black.svg" import CheckIndeterminate from "Core/__deprecated__/renderer/svg/check-indeterminate.svg" import Close from "Core/__deprecated__/renderer/svg/close.svg" import CloseWhite from "Core/__deprecated__/renderer/svg/close-white.svg" @@ -125,9 +127,11 @@ import Warning from "Core/__deprecated__/renderer/svg/warning.svg" import MarkAsUnread from "Core/__deprecated__/renderer/svg/mark-as-unread.svg" import Conversation from "Core/__deprecated__/renderer/svg/conversation.svg" import Exclamation from "Core/__deprecated__/renderer/svg/exclamation.svg" +import ButtonSuccess from "Core/__deprecated__/renderer/svg/button-success.svg" import DataMigration from "../../../../../../generic-view/ui/src/lib/icon/svg/data-migration.svg" import RecoveryModeWhite from "../../../../../../generic-view/ui/src/lib/icon/svg/recovery-mode-white.svg" import RecoveryModeBlack from "../../../../../../generic-view/ui/src/lib/icon/svg/recovery-mode-black.svg" +import LightButton from "Core/__deprecated__/renderer/svg/light-button-icon.svg" import { FunctionComponent } from "Core/core/types/function-component.interface" import { IconType } from "Core/__deprecated__/renderer/components/core/icon/icon-type" @@ -146,6 +150,9 @@ const typeToIcon: Partial> = { [IconType.AttachContact]: AttachContact, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + [IconType.BackArrowIcon]: BackArrowIcon, + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment [IconType.BackupFolder]: BackupFolder, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -170,6 +177,9 @@ const typeToIcon: Partial> = { [IconType.CheckCircle]: CheckCircle, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + [IconType.CheckCircleBlack]: CheckCircleBlack, + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment [IconType.CheckIndeterminate]: CheckIndeterminate, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -498,6 +508,8 @@ const typeToIcon: Partial> = { [IconType.DataMigration]: DataMigration, [IconType.RecoveryModeWhite]: RecoveryModeWhite, [IconType.RecoveryModeBlack]: RecoveryModeBlack, + [IconType.ButtonSuccess]: ButtonSuccess, + [IconType.LightButton]: LightButton, } export const getIconType = ( diff --git a/libs/core/__deprecated__/renderer/components/core/text/text.component.tsx b/libs/core/__deprecated__/renderer/components/core/text/text.component.tsx index 942a55012f..b72c909919 100644 --- a/libs/core/__deprecated__/renderer/components/core/text/text.component.tsx +++ b/libs/core/__deprecated__/renderer/components/core/text/text.component.tsx @@ -121,6 +121,7 @@ export interface TextProps { readonly onClick?: () => void readonly testId?: string readonly textRef?: React.Ref + readonly id?: string } export enum TextDisplayStyle { @@ -185,7 +186,6 @@ const Text: FunctionComponent = ({ {message && typeof message !== "string" && ( )} - {!message && children} ) diff --git a/libs/core/__deprecated__/renderer/components/rest/header/__snapshots__/header.test.tsx.snap b/libs/core/__deprecated__/renderer/components/rest/header/__snapshots__/header.test.tsx.snap index 31e38428d9..cf836f30fe 100644 --- a/libs/core/__deprecated__/renderer/components/rest/header/__snapshots__/header.test.tsx.snap +++ b/libs/core/__deprecated__/renderer/components/rest/header/__snapshots__/header.test.tsx.snap @@ -43,6 +43,7 @@ exports[`matches snapshot without tabs 1`] = ` class="c1 c2" color="primary" data-testid="location" + id="app-header" > [value] module.overview diff --git a/libs/core/__deprecated__/renderer/components/rest/header/header.component.tsx b/libs/core/__deprecated__/renderer/components/rest/header/header.component.tsx index 3d319f3adf..b765349bc1 100644 --- a/libs/core/__deprecated__/renderer/components/rest/header/header.component.tsx +++ b/libs/core/__deprecated__/renderer/components/rest/header/header.component.tsx @@ -122,6 +122,7 @@ const Header: FunctionComponent = ({ ) : ( OS 1.9.0 or later.", "module.recoveryMode.harmony.warning2": "Once you start the recovery, it must not be cancelled or interrupted.", - "module.recoveryMode.harmony.warning3": "Do not disconnect your Harmony during the recovery process.", + "module.recoveryMode.harmony.warning3": "Connect your Harmony directly to your computer using the USB C cable and do not disconnect it until the process is complete.", "module.recoveryMode.harmony.warning4": "Before starting the recovery, charge your device for 1 hour (or more) from a suitable power outlet.", + "module.recoveryMode.harmony.warningLinux": "Make sure your computer is running Ubuntu 22.04 or later.", "module.recoveryMode.harmony.confirmation": "I understand that not following these instructions may void the warranty", "module.recoveryMode.harmony.action": "Start Recovery", + "module.recoveryMode.modal.message": "Recovery mode in progress...", + "module.recoveryMode.modal.warning": "Warning!", + "module.recoveryMode.modal.warningMessage": "Do not disconnect Harmony or interrupt the process!", + "module.recoveryMode.modal.step0": "Initializing...", + "module.recoveryMode.modal.step1": "Configuration downloading... (1 of 4)", + "module.recoveryMode.modal.step2": "Image downloading... (2 of 4)", + "module.recoveryMode.modal.step3": "Image unpacking... (3 of 4)", + "module.recoveryMode.modal.step4": "Flashing process... (4 of 4)", + "module.recoveryMode.modal.restarting.subtitle": "Restarting Harmony...", + "module.recoveryMode.modal.restarting.message": "Please do not disconnect your Harmony.", + "module.recoveryMode.modal.error.subtitle": "Recovery failed", + "module.recoveryMode.modal.error.message": "The process was interrupted. Please try again.", + "module.recoveryMode.modal.waitingForBackButton.subtitle": "Almost finished...", + "module.recoveryMode.modal.waitingForBackButton.description": "Press the back button on your device to complete the recovery process.", + "module.recoveryMode.modal.terminalInfo.subtitle": "Just a few more steps...", + "module.recoveryMode.modal.terminalInfo.description": "The Terminal was opened in the background", + "module.recoveryMode.modal.terminalInfo.step1": "1. Switch to the open terminal", + "module.recoveryMode.modal.terminalInfo.step2": "2. Press enter to start the process", + "module.recoveryMode.modal.terminalInfo.step3": "3. Enter your computer password when prompted", + "module.recoveryMode.modal.terminalInfo.step4": "4. Repeat your computer password when prompted", + "module.recoveryMode.modal.completed.subtitle": "Recovery process complete", + "module.recoveryMode.modal.completed.description": "Harmony is in transport mode. Press the light button when \"ON\" and \"OFF\" appear on harmony's screen.", "module.genericViews.dataMigration.cancelConfirm.title": "Cancel data transfer?", "module.genericViews.dataMigration.cancelConfirm.description": "We’ll stop the transfer but some data may already be on your Kompakt.", "module.genericViews.dataMigration.cancelConfirm.cancelButtonLabel": "Cancel transfer", diff --git a/libs/core/__deprecated__/renderer/store/reducers.ts b/libs/core/__deprecated__/renderer/store/reducers.ts index eed1d29773..d87c7d9eaa 100644 --- a/libs/core/__deprecated__/renderer/store/reducers.ts +++ b/libs/core/__deprecated__/renderer/store/reducers.ts @@ -32,10 +32,14 @@ import { genericViewsReducer, importsReducer, externalProvidersReducer, + genericEntitiesReducer, + genericToastsReducer, } from "generic-view/store" import { appStateReducer } from "shared/app-state" import { activeDeviceRegistryReducer } from "active-device-registry/feature" import { helpReducer } from "help/store" +import { timeSynchronizationReducer } from "Core/time-synchronization/reducers/time-synchronization.reducer" +import { flashingReducer } from "msc-flash-harmony" export const reducers = { device: deviceReducer, @@ -52,6 +56,7 @@ export const reducers = { news: newsReducer, settings: settingsReducer, update: updateOsReducer, + timeSynchronization: timeSynchronizationReducer, discoveryDevice: discoveryDeviceReducer, deviceInitialization: deviceInitializationReducer, appInitialization: appInitializationReducer, @@ -68,6 +73,9 @@ export const reducers = { dataMigration: dataMigrationReducer, genericDataTransfer: genericDataTransferReducer, helpV2: helpReducer, + flashing: flashingReducer, + genericEntities: genericEntitiesReducer, + genericToasts: genericToastsReducer, } export const combinedReducers = combineReducers(reducers) diff --git a/libs/core/__deprecated__/renderer/svg/back-arrow-icon.svg b/libs/core/__deprecated__/renderer/svg/back-arrow-icon.svg new file mode 100644 index 0000000000..f4d4d9c916 --- /dev/null +++ b/libs/core/__deprecated__/renderer/svg/back-arrow-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/libs/core/__deprecated__/renderer/svg/button-success.svg b/libs/core/__deprecated__/renderer/svg/button-success.svg new file mode 100644 index 0000000000..db74189fb2 --- /dev/null +++ b/libs/core/__deprecated__/renderer/svg/button-success.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/libs/core/__deprecated__/renderer/svg/check-circle-black.svg b/libs/core/__deprecated__/renderer/svg/check-circle-black.svg new file mode 100644 index 0000000000..ec0c92a278 --- /dev/null +++ b/libs/core/__deprecated__/renderer/svg/check-circle-black.svg @@ -0,0 +1,4 @@ + + + + diff --git a/libs/core/__deprecated__/renderer/svg/light-button-icon.svg b/libs/core/__deprecated__/renderer/svg/light-button-icon.svg new file mode 100644 index 0000000000..b427a42bc2 --- /dev/null +++ b/libs/core/__deprecated__/renderer/svg/light-button-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/libs/core/analytic-data-tracker/services/analytic-data-tracker.factory.test.ts b/libs/core/analytic-data-tracker/services/analytic-data-tracker.factory.test.ts index 2d21de225f..40a56c9f44 100644 --- a/libs/core/analytic-data-tracker/services/analytic-data-tracker.factory.test.ts +++ b/libs/core/analytic-data-tracker/services/analytic-data-tracker.factory.test.ts @@ -13,6 +13,17 @@ import { getSettingsService } from "Core/settings/containers" import { SettingsService } from "Core/settings/services" import { FileSystemService } from "Core/file-system/services/file-system.service.refactored" +jest.mock("history", () => ({ + createHashHistory: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + block: jest.fn(), + listen: jest.fn(), + location: { pathname: "", search: "", hash: "", state: null }, + })), +})) + jest.mock("Core/settings/containers/settings.container") jest.mock("Core/__deprecated__/main/utils/logger") jest.mock("axios") diff --git a/libs/core/backup/backup.module.ts b/libs/core/backup/backup.module.ts index 75a723a378..bd15fea22a 100644 --- a/libs/core/backup/backup.module.ts +++ b/libs/core/backup/backup.module.ts @@ -48,6 +48,7 @@ export class BackupModule extends BaseModule { const deviceFileSystem = new DeviceFileSystemService(this.deviceProtocol) const fileManagerService = new FileManagerService( + this.deviceProtocol, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-call new FileDeleteCommand(this.deviceProtocol), diff --git a/libs/core/backup/controllers/backup.controller.ts b/libs/core/backup/controllers/backup.controller.ts index 19d8ffdb4a..9dbd9e4859 100644 --- a/libs/core/backup/controllers/backup.controller.ts +++ b/libs/core/backup/controllers/backup.controller.ts @@ -27,10 +27,11 @@ export class BackupController { } @IpcEvent(IpcBackupEvent.CreateBackup) - public async createBackup( - data: CreateDeviceBackup - ): Promise> { - return this.backupCreateService.createBackup(data) + public async createBackup({ + deviceId, + ...data + }: CreateDeviceBackup): Promise> { + return this.backupCreateService.createBackup(data, deviceId) } @IpcEvent(IpcBackupEvent.RestoreBackup) diff --git a/libs/core/backup/services/backup-create.service.test.ts b/libs/core/backup/services/backup-create.service.test.ts index 680f45af28..788574f2b4 100644 --- a/libs/core/backup/services/backup-create.service.test.ts +++ b/libs/core/backup/services/backup-create.service.test.ts @@ -16,6 +16,17 @@ import { DeviceFileSystemService } from "Core/device-file-system/services" import { FileManagerService } from "Core/files-manager/services" import { DeviceInfoService } from "Core/device-info/services" +jest.mock("history", () => ({ + createHashHistory: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + block: jest.fn(), + listen: jest.fn(), + location: { pathname: "", search: "", hash: "", state: null }, + })), +})) + const updaterStatusSuccessMock: UpdaterStatus = { branch: "", message: "", @@ -29,6 +40,7 @@ const deviceProtocol = { device: { request: jest.fn(), }, + request: jest.fn(), } as unknown as DeviceProtocol const deviceFileSystemAdapter = { @@ -63,25 +75,27 @@ beforeEach(() => { describe("Backup process happy path", () => { test("Returns the `Result.success` object with backup data", async () => { - deviceProtocol.device.request = jest + deviceProtocol.request = jest .fn() - .mockImplementation((config: { endpoint: Endpoint; method: Method }) => { - if ( - config.endpoint === Endpoint.Backup && - config.method === Method.Post - ) { - return Result.success(true) - } - - if ( - config.endpoint === Endpoint.Security && - config.method === Method.Get - ) { - return Result.success(true) + .mockImplementation( + (deviceId, config: { endpoint: Endpoint; method: Method }) => { + if ( + config.endpoint === Endpoint.Backup && + config.method === Method.Post + ) { + return Result.success(true) + } + + if ( + config.endpoint === Endpoint.Security && + config.method === Method.Get + ) { + return Result.success(true) + } + + return Result.failed(new AppError("", "")) } - - return Result.failed(new AppError("", "")) - }) + ) deviceInfoService.getDeviceInfo = jest.fn().mockResolvedValue( Result.success({ backupFilePath: "/user/local/backup/fileBase.tar", @@ -114,14 +128,14 @@ describe("Backup process happy path", () => { expect(result).toEqual(Result.success(["/user/backup/backup.tar"])) // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/unbound-method - expect(deviceProtocol.device.request).toHaveBeenNthCalledWith(1, { + expect(deviceProtocol.request).toHaveBeenNthCalledWith(1, undefined, { endpoint: Endpoint.Backup, method: Method.Post, body: { category: BackupCategory.Backup }, }) // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/unbound-method - expect(deviceProtocol.device.request).toHaveBeenNthCalledWith(2, { + expect(deviceProtocol.request).toHaveBeenNthCalledWith(2, undefined, { endpoint: Endpoint.Security, method: Method.Get, body: { category: PhoneLockCategory.Status }, @@ -129,16 +143,21 @@ describe("Backup process happy path", () => { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/unbound-method expect(deviceFileSystemAdapter.downloadFile).toHaveBeenLastCalledWith( - "/user/local/recovery/updater_status.json" + "/user/local/recovery/updater_status.json", + undefined ) expect( // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/unbound-method deviceFileSystemAdapter.downloadDeviceFilesLocally - ).toHaveBeenLastCalledWith(["/user/local/backup/fileBase.tar"], { - key: "1234", - cwd: "/User/documents/backup", - }) + ).toHaveBeenLastCalledWith( + ["/user/local/backup/fileBase.tar"], + { + key: "1234", + cwd: "/User/documents/backup", + }, + undefined + ) // TODO: please remove custom timeout }, 10000) }) @@ -194,7 +213,7 @@ describe("Backup process failed path", () => { }) test("Returns the `Result.failed` with `BackupError.CannotReachBackupLocation` if `DeviceInfo` endpoint return error status", async () => { - deviceProtocol.device.request = jest + deviceProtocol.request = jest .fn() .mockResolvedValue(Result.failed(new AppError("", ""))) deviceInfoService.getDeviceInfo = jest.fn().mockResolvedValue( @@ -226,7 +245,7 @@ describe("Backup process failed path", () => { }) test("Returns the `Result.failed` with `BackupError.CannotReachBackupLocation` if `Backup` endpoint return error status", async () => { - deviceProtocol.device.request = jest + deviceProtocol.request = jest .fn() .mockResolvedValue(Result.failed(new AppError("", ""))) deviceInfoService.getDeviceInfo = jest.fn().mockResolvedValue( @@ -259,25 +278,27 @@ describe("Backup process failed path", () => { }) test("Returns the `Result.failed` with `BackupError.BackupProcessFailed` if `deviceFileSystem.downloadFile` returns error status", async () => { - deviceProtocol.device.request = jest + deviceProtocol.request = jest .fn() - .mockImplementation((config: { endpoint: Endpoint; method: Method }) => { - if ( - config.endpoint === Endpoint.Backup && - config.method === Method.Post - ) { - return Result.success(true) + .mockImplementation( + (deviceId, config: { endpoint: Endpoint; method: Method }) => { + if ( + config.endpoint === Endpoint.Backup && + config.method === Method.Post + ) { + return Result.success(true) + } + + if ( + config.endpoint === Endpoint.Security && + config.method === Method.Get + ) { + return Result.success(true) + } + + return Result.failed(new AppError("", "")) } - - if ( - config.endpoint === Endpoint.Security && - config.method === Method.Get - ) { - return Result.success(true) - } - - return Result.failed(new AppError("", "")) - }) + ) deviceInfoService.getDeviceInfo = jest.fn().mockResolvedValue( Result.success({ backupFilePath: "/user/local/backup/backupFilePath", @@ -312,25 +333,27 @@ describe("Backup process failed path", () => { }, 10000) test("Returns the `Result.failed` with `BackupError.BackupDownloadFailed` if `deviceFileSystem.downloadDeviceFilesLocally` returns error status", async () => { - deviceProtocol.device.request = jest + deviceProtocol.request = jest .fn() - .mockImplementation((config: { endpoint: Endpoint; method: Method }) => { - if ( - config.endpoint === Endpoint.Backup && - config.method === Method.Post - ) { - return Result.success(true) + .mockImplementation( + (deviceId, config: { endpoint: Endpoint; method: Method }) => { + if ( + config.endpoint === Endpoint.Backup && + config.method === Method.Post + ) { + return Result.success(true) + } + + if ( + config.endpoint === Endpoint.Security && + config.method === Method.Get + ) { + return Result.success(true) + } + + return Result.failed(new AppError("", "")) } - - if ( - config.endpoint === Endpoint.Security && - config.method === Method.Get - ) { - return Result.success(true) - } - - return Result.failed(new AppError("", "")) - }) + ) deviceInfoService.getDeviceInfo = jest.fn().mockResolvedValue( Result.success({ backupFilePath: "/user/local/backup/backupFilePath", diff --git a/libs/core/backup/services/backup-create.service.ts b/libs/core/backup/services/backup-create.service.ts index 27a5388f5a..30cdd48984 100644 --- a/libs/core/backup/services/backup-create.service.ts +++ b/libs/core/backup/services/backup-create.service.ts @@ -9,13 +9,15 @@ import { BackupCategory } from "Core/device/constants" import { Result, ResultObject } from "Core/core/builder" import { AppError } from "Core/core/errors" import { BackupError, Operation } from "Core/backup/constants" -import { MetadataStore, MetadataKey } from "Core/metadata" +import { MetadataKey, MetadataStore } from "Core/metadata" import { CreateDeviceBackup } from "Core/backup/types" import { DeviceFileSystemService } from "Core/device-file-system/services" import { BaseBackupService } from "Core/backup/services/base-backup.service" import { FileManagerService } from "Core/files-manager/services" import { DeviceDirectory } from "Core/files-manager/constants" import { DeviceInfoService } from "Core/device-info/services" +import { extract } from "tar" +import { remove } from "fs-extra" export class BackupCreateService extends BaseBackupService { constructor( @@ -29,7 +31,8 @@ export class BackupCreateService extends BaseBackupService { } public async createBackup( - options: CreateDeviceBackup + options: CreateDeviceBackup, + deviceId = this.deviceProtocol.device.id ): Promise> { if (this.keyStorage.getValue(MetadataKey.BackupInProgress)) { return Result.failed( @@ -40,14 +43,14 @@ export class BackupCreateService extends BaseBackupService { this.keyStorage.setValue(MetadataKey.BackupInProgress, true) const validateRequiredBackupSpaceResult = - await this.validateRequiredBackupSpace() + await this.validateRequiredBackupSpace(deviceId) if (!validateRequiredBackupSpaceResult.ok) { this.keyStorage.setValue(MetadataKey.BackupInProgress, false) return validateRequiredBackupSpaceResult } - const runDeviceBackupResult = await this.runDeviceBackup() + const runDeviceBackupResult = await this.runDeviceBackup(deviceId) if (!runDeviceBackupResult.ok || !runDeviceBackupResult.data) { this.keyStorage.setValue(MetadataKey.BackupInProgress, false) @@ -60,7 +63,7 @@ export class BackupCreateService extends BaseBackupService { ) } - const operationStatus = await this.checkStatus(Operation.Backup) + const operationStatus = await this.checkStatus(Operation.Backup, deviceId) if (!operationStatus.ok) { this.keyStorage.setValue(MetadataKey.BackupInProgress, false) @@ -78,7 +81,8 @@ export class BackupCreateService extends BaseBackupService { const backupFileResult = await this.deviceFileSystem.downloadDeviceFilesLocally( [filePath], - options + options, + deviceId ) if (!backupFileResult.ok || !backupFileResult.data) { @@ -90,15 +94,27 @@ export class BackupCreateService extends BaseBackupService { } // TODO: Moved removing backup logic to OS - await this.deviceFileSystem.removeDeviceFile(filePath) + await this.deviceFileSystem.removeDeviceFile(filePath, deviceId) this.keyStorage.setValue(MetadataKey.BackupInProgress, false) + if (options.extract) { + await extract({ + file: backupFileResult.data[0], + cwd: options.cwd, + }) + await remove(backupFileResult.data[0]) + } + return Result.success(backupFileResult.data) } - private async runDeviceBackup(): Promise> { - const deviceInfoResult = await this.deviceInfoService.getDeviceInfo() + private async runDeviceBackup( + deviceId = this.deviceProtocol.device.id + ): Promise> { + const deviceInfoResult = await this.deviceInfoService.getDeviceInfo( + deviceId + ) if (!deviceInfoResult.ok || !deviceInfoResult.data) { return Result.failed( @@ -109,7 +125,7 @@ export class BackupCreateService extends BaseBackupService { ) } - const backupResponse = await this.deviceProtocol.device.request({ + const backupResponse = await this.deviceProtocol.request(deviceId, { endpoint: Endpoint.Backup, method: Method.Post, body: { @@ -126,7 +142,7 @@ export class BackupCreateService extends BaseBackupService { ) } - const backupFinished = await this.waitUntilProcessFinished() + const backupFinished = await this.waitUntilProcessFinished(deviceId) if (!backupFinished.ok && backupFinished.error) { return Result.failed( @@ -139,12 +155,15 @@ export class BackupCreateService extends BaseBackupService { return Result.success(filePath) } - private async validateRequiredBackupSpace(): Promise< - ResultObject - > { - const getDeviceFilesResult = await this.fileManagerService.getDeviceFiles({ - directory: DeviceDirectory.DB, - }) + private async validateRequiredBackupSpace( + deviceId = this.deviceProtocol.device.id + ): Promise> { + const getDeviceFilesResult = await this.fileManagerService.getDeviceFiles( + { + directory: DeviceDirectory.DB, + }, + deviceId + ) if (!getDeviceFilesResult.ok || getDeviceFilesResult.data === undefined) { return Result.failed( @@ -160,8 +179,10 @@ export class BackupCreateService extends BaseBackupService { }, 0) // 100 MiB as buffer space - const backupSizeRequaired = backupSize * 2 + 100 - const deviceInfoResult = await this.deviceInfoService.getDeviceInfo() + const backupSizeRequired = backupSize * 2 + 100 + const deviceInfoResult = await this.deviceInfoService.getDeviceInfo( + deviceId + ) if (!deviceInfoResult.ok || !deviceInfoResult.data) { return Result.failed( @@ -175,14 +196,14 @@ export class BackupCreateService extends BaseBackupService { deviceInfoResult.data.memorySpace const free = total - usedUserSpace - reservedSpace - if (backupSizeRequaired <= free) { + if (backupSizeRequired <= free) { return Result.success(undefined) } else { return Result.failed( new AppError( BackupError.BackupSpaceIsNotEnough, `Backup space is not enough`, - backupSizeRequaired + backupSizeRequired ) ) } diff --git a/libs/core/backup/services/backup-restore.service.test.ts b/libs/core/backup/services/backup-restore.service.test.ts index aedd2b3297..3698d5fc20 100644 --- a/libs/core/backup/services/backup-restore.service.test.ts +++ b/libs/core/backup/services/backup-restore.service.test.ts @@ -15,9 +15,19 @@ import { BackupError, Operation } from "Core/backup/constants" import { UpdaterStatus } from "Core/backup/dto" import { FileSystemService } from "Core/file-system/services/file-system.service.refactored" import { DeviceFileSystemService } from "Core/device-file-system/services" -import { PhoneLockCategory } from "Core/device" import { DeviceInfoService } from "Core/device-info/services" +jest.mock("history", () => ({ + createHashHistory: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + block: jest.fn(), + listen: jest.fn(), + location: { pathname: "", search: "", hash: "", state: null }, + })), +})) + const arrayBufferToBuffer = (unitArray: Uint8Array): Buffer => { const buffer = Buffer.alloc(unitArray.byteLength) const view = new Uint8Array(unitArray) @@ -65,6 +75,7 @@ const deviceProtocol = { device: { request: jest.fn(), }, + request: jest.fn(), } as unknown as DeviceProtocol const deviceFileSystemAdapter = { @@ -111,6 +122,27 @@ describe("Restore process happy path", () => { .mockResolvedValueOnce( Result.success(JSON.stringify(updaterStatusSuccessMock)) ) + deviceProtocol.request = jest + .fn() + .mockImplementation( + (deviceId, config: { endpoint: Endpoint; method: Method }) => { + if ( + config.endpoint === Endpoint.Restore && + config.method === Method.Post + ) { + return Result.success(true) + } + + if ( + config.endpoint === Endpoint.Security && + config.method === Method.Get + ) { + return Result.success(true) + } + + return Result.failed(new AppError("", "")) + } + ) deviceProtocol.device.request = jest .fn() .mockImplementation((config: { endpoint: Endpoint; method: Method }) => { @@ -169,10 +201,10 @@ describe("Restore process happy path", () => { }) // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/unbound-method - expect(deviceProtocol.device.request).toHaveBeenNthCalledWith(2, { - endpoint: Endpoint.Security, - method: Method.Get, - body: { category: PhoneLockCategory.Status }, + expect(deviceProtocol.request).toHaveBeenNthCalledWith(1, undefined, { + body: { category: "phoneLockStatus" }, + endpoint: 13, + method: 1, }) // TODO: please remove custom timeout }, 10000) @@ -263,7 +295,7 @@ describe("Backup restoring failed path", () => { deviceFileSystemAdapter.uploadFile = jest .fn() .mockResolvedValueOnce(Result.success(true)) - deviceProtocol.device.request = jest + deviceProtocol.request = jest .fn() .mockImplementation((config: { endpoint: Endpoint; method: Method }) => { if ( @@ -275,7 +307,7 @@ describe("Backup restoring failed path", () => { return Result.failed(new AppError("", "")) }) - deviceProtocol.device.request = jest + deviceProtocol.request = deviceProtocol.device.request = jest .fn() .mockResolvedValue(Result.failed(new AppError("", ""))) deviceInfoService.getDeviceInfo = jest.fn().mockResolvedValue( @@ -310,6 +342,27 @@ describe("Backup restoring failed path", () => { deviceFileSystemAdapter.downloadFile = jest .fn() .mockResolvedValueOnce(Result.failed(new AppError("", ""))) + deviceProtocol.request = jest + .fn() + .mockImplementation( + (deviceId, config: { endpoint: Endpoint; method: Method }) => { + if ( + config.endpoint === Endpoint.Restore && + config.method === Method.Post + ) { + return Result.success(true) + } + + if ( + config.endpoint === Endpoint.Security && + config.method === Method.Get + ) { + return Result.success(true) + } + + return Result.failed(new AppError("", "")) + } + ) deviceProtocol.device.request = jest .fn() .mockImplementation((config: { endpoint: Endpoint; method: Method }) => { @@ -371,6 +424,27 @@ describe("Backup restoring failed path", () => { JSON.stringify(updaterStatusSuccessForAnotherOperationMock) ) ) + deviceProtocol.request = jest + .fn() + .mockImplementation( + (deviceId, config: { endpoint: Endpoint; method: Method }) => { + if ( + config.endpoint === Endpoint.Restore && + config.method === Method.Post + ) { + return Result.success(true) + } + + if ( + config.endpoint === Endpoint.Security && + config.method === Method.Get + ) { + return Result.success(true) + } + + return Result.failed(new AppError("", "")) + } + ) deviceProtocol.device.request = jest .fn() .mockImplementation((config: { endpoint: Endpoint; method: Method }) => { @@ -430,6 +504,27 @@ describe("Backup restoring failed path", () => { .mockResolvedValueOnce( Result.success(JSON.stringify(updaterStatusFailedMock)) ) + deviceProtocol.request = jest + .fn() + .mockImplementation( + (deviceId, config: { endpoint: Endpoint; method: Method }) => { + if ( + config.endpoint === Endpoint.Restore && + config.method === Method.Post + ) { + return Result.success(true) + } + + if ( + config.endpoint === Endpoint.Security && + config.method === Method.Get + ) { + return Result.success(true) + } + + return Result.failed(new AppError("", "")) + } + ) deviceProtocol.device.request = jest .fn() .mockImplementation((config: { endpoint: Endpoint; method: Method }) => { diff --git a/libs/core/backup/services/base-backup.service.ts b/libs/core/backup/services/base-backup.service.ts index c8f862728c..b68f3a11df 100644 --- a/libs/core/backup/services/base-backup.service.ts +++ b/libs/core/backup/services/base-backup.service.ts @@ -24,9 +24,10 @@ export class BaseBackupService { ) {} public async checkStatus( - operation: Operation + operation: Operation, + deviceId = this.deviceProtocol.device.id ): Promise> { - const deviceResponse = await this.deviceInfoService.getDeviceInfo() + const deviceResponse = await this.deviceInfoService.getDeviceInfo(deviceId) if (!deviceResponse.ok || !deviceResponse.data) { return Result.failed( @@ -38,7 +39,8 @@ export class BaseBackupService { } const response = await this.deviceFileSystem.downloadFile( - deviceResponse.data.recoveryStatusFilePath + deviceResponse.data.recoveryStatusFilePath, + deviceId ) if (!response.ok || !response.data) { @@ -73,10 +75,13 @@ export class BaseBackupService { } } - public async waitUntilProcessFinished(): Promise< - ResultObject - > { - const wakeUpResponse = await this.waitUntilDeviceResponse() + public async waitUntilProcessFinished( + deviceId = this.deviceProtocol.device.id + ): Promise> { + const wakeUpResponse = await this.waitUntilDeviceResponse( + undefined, + deviceId + ) if (!wakeUpResponse) { return Result.failed( @@ -84,7 +89,10 @@ export class BaseBackupService { ) } - const unlockResponse = await this.waitUntilGetUnlockDeviceStatusResponse() + const unlockResponse = await this.waitUntilGetUnlockDeviceStatusResponse( + undefined, + deviceId + ) if (unlockResponse) { return Result.success(true) @@ -95,7 +103,10 @@ export class BaseBackupService { } } - private async waitUntilDeviceResponse(index = 0): Promise { + private async waitUntilDeviceResponse( + index = 0, + deviceId = this.deviceProtocol.device.id + ): Promise { if (index === this.MAX_WAKE_UP_RETRIES) { return false } @@ -104,16 +115,18 @@ export class BaseBackupService { setTimeout(() => { void (async () => { try { - const response = await this.deviceInfoService.getDeviceInfo() + const response = await this.deviceInfoService.getDeviceInfo( + deviceId + ) if (response.ok) { resolve(true) return } - resolve(this.waitUntilDeviceResponse(++index)) + resolve(this.waitUntilDeviceResponse(++index, deviceId)) } catch (e) { - resolve(this.waitUntilDeviceResponse(++index)) + resolve(this.waitUntilDeviceResponse(++index, deviceId)) } })() }, this.REQUEST_TIME_OUT) @@ -121,13 +134,14 @@ export class BaseBackupService { } private async waitUntilGetUnlockDeviceStatusResponse( - index = 0 + index = 0, + deviceId = this.deviceProtocol.device.id ): Promise { if (index === this.REQUEST_TIME_OUT) { return false } - const response = await this.deviceProtocol.device.request({ + const response = await this.deviceProtocol.request(deviceId, { endpoint: Endpoint.Security, method: Method.Get, body: { category: PhoneLockCategory.Status }, @@ -138,7 +152,9 @@ export class BaseBackupService { } else { return new Promise((resolve) => { setTimeout(() => { - resolve(this.waitUntilGetUnlockDeviceStatusResponse(++index)) + resolve( + this.waitUntilGetUnlockDeviceStatusResponse(++index, deviceId) + ) }, this.REQUEST_TIME_OUT) }) } diff --git a/libs/core/backup/types/create-device-backup.type.ts b/libs/core/backup/types/create-device-backup.type.ts index 914eb2cd4a..36a90ddedd 100644 --- a/libs/core/backup/types/create-device-backup.type.ts +++ b/libs/core/backup/types/create-device-backup.type.ts @@ -3,10 +3,13 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +import { DeviceId } from "Core/device/constants/device-id" + export interface CreateDeviceBackup { cwd: string token?: string extract?: boolean fileBase?: string key?: string + deviceId?: DeviceId } diff --git a/libs/core/contact-support/components/contact-support-modal-error.component.tsx b/libs/core/contact-support/components/contact-support-modal-error.component.tsx index a5a5327055..f731d802c5 100644 --- a/libs/core/contact-support/components/contact-support-modal-error.component.tsx +++ b/libs/core/contact-support/components/contact-support-modal-error.component.tsx @@ -32,10 +32,12 @@ export const ContactSupportModalError: FunctionComponent = ({ diff --git a/libs/core/contact-support/components/contact-support-modal-success.component.tsx b/libs/core/contact-support/components/contact-support-modal-success.component.tsx index f51cd23ee1..674407149d 100644 --- a/libs/core/contact-support/components/contact-support-modal-success.component.tsx +++ b/libs/core/contact-support/components/contact-support-modal-success.component.tsx @@ -5,11 +5,12 @@ import React from "react" import { defineMessages } from "react-intl" -import { FunctionComponent } from "Core/core/types/function-component.interface" +import { ContactSupportModalTestIds } from "e2e-test-ids" import { Modal } from "generic-view/ui" +import { IconType } from "generic-view/utils" +import { FunctionComponent } from "Core/core/types/function-component.interface" import { intl } from "Core/__deprecated__/renderer/utils/intl" import { ButtonSecondary } from "../../../generic-view/ui/src/lib/buttons/button-secondary" -import { IconType } from "generic-view/utils" const messages = defineMessages({ title: { id: "component.supportModalSuccessTitle" }, @@ -26,16 +27,23 @@ export const ContactSupportModalSuccess: FunctionComponent = ({ }) => ( <> - {intl.formatMessage(messages.title)} -

{intl.formatMessage(messages.body)}

+ + {intl.formatMessage(messages.title)} + +

+ {intl.formatMessage(messages.body)} +

diff --git a/libs/core/contact-support/components/contact-support-modal.component.test.tsx b/libs/core/contact-support/components/contact-support-modal.component.test.tsx index 2312575fb4..1657bcf933 100644 --- a/libs/core/contact-support/components/contact-support-modal.component.test.tsx +++ b/libs/core/contact-support/components/contact-support-modal.component.test.tsx @@ -12,6 +12,19 @@ import { ContactSupportModalTestIds } from "Core/contact-support/components/cont import { noop } from "Core/__deprecated__/renderer/utils/noop" import { FileListTestIds } from "Core/__deprecated__/renderer/components/core/file-list/file-list-test-ids.enum" +jest.mock("e2e-test-ids", () => { + return { + NewContactSupportModalTestIds: { + Title: "contact-support-modal-title", + Subtitle: "contact-support-modal-subtitle", + EmailLabel: "email-label", + MessageLabel: "message-label", + AttachedFilesLabel: "attached-files-label", + AttachedFilesSubtext: "attached-files-subtext", + }, + } +}) + type Props = ComponentProps const defaultProps: Props = { open: true, diff --git a/libs/core/contact-support/components/contact-support-modal.component.tsx b/libs/core/contact-support/components/contact-support-modal.component.tsx index 96e25a9ee7..f60a60b667 100644 --- a/libs/core/contact-support/components/contact-support-modal.component.tsx +++ b/libs/core/contact-support/components/contact-support-modal.component.tsx @@ -36,6 +36,7 @@ import { HelpActions } from "Core/__deprecated__/common/enums/help-actions.enum" import { ModalTestIds } from "Core/__deprecated__/renderer/components/core/modal/modal-test-ids.enum" import { Close } from "Core/__deprecated__/renderer/components/core/modal/modal.styled.elements" import { SpinnerLoader } from "../../../generic-view/ui/src/lib/shared/spinner-loader" +import { NewContactSupportModalTestIds } from "e2e-test-ids" const messages = defineMessages({ actionButton: { @@ -95,8 +96,12 @@ interface FormInputLabelProps { export const FormInputLabelComponent: FunctionComponent< FormInputLabelProps -> = ({ className, label, optional }) => ( - +> = ({ className, label, optional, ...rest }) => ( + {optional && ( = ({ -

+

-

+

- + = ({ label={intl.formatMessage(messages.emailPlaceholder)} {...register(FieldKeys.Email, emailValidator)} /> - + = ({ data-testid={ContactSupportModalTestIds.DescriptionInput} {...register(FieldKeys.Description)} /> - + ({ generateApplicationId: () => "123", })) +jest.mock("e2e-test-ids", () => { + return { + ModalTestIds: { + Modal: "modal-content", + }, + NewContactSupportModalTestIds: { + Title: "contact-support-modal-title", + Subtitle: "contact-support-modal-subtitle", + EmailLabel: "email-label", + MessageLabel: "message-label", + AttachedFilesLabel: "attached-files-label", + AttachedFilesSubtext: "attached-files-subtext", + }, + } +}) + type Props = ComponentProps const defaultProps: Props = {} diff --git a/libs/core/contacts/components/contact-edit/contact-edit.component.tsx b/libs/core/contacts/components/contact-edit/contact-edit.component.tsx index 29976af023..c0a2710d3b 100644 --- a/libs/core/contacts/components/contact-edit/contact-edit.component.tsx +++ b/libs/core/contacts/components/contact-edit/contact-edit.component.tsx @@ -211,7 +211,10 @@ const ContactEdit: FunctionComponent = ({ = ({ = ({ @@ -260,7 +266,10 @@ const ContactEdit: FunctionComponent = ({ = ({ ( key="help-category-article" path={`${URL_MAIN.help}/:categoryId/:articleId`} component={ArticlePage} - />, + /> + , , - + , + { useFileDialogEventListener() useHelp() + // MSC + useMscDeviceDetachedEffect() + return ( <> diff --git a/libs/core/core/hooks/use-device-detached-effect.ts b/libs/core/core/hooks/use-device-detached-effect.ts index f217180fc8..cae52f25c5 100644 --- a/libs/core/core/hooks/use-device-detached-effect.ts +++ b/libs/core/core/hooks/use-device-detached-effect.ts @@ -39,7 +39,6 @@ export const useDeviceDetachedEffect = () => { } const useHandleDevicesDetached = () => { - const dispatch = useDispatch() const processActiveDevicesDetachment = useProcessActiveDevicesDetachment() const processSingleDeviceRemaining = useProcessSingleDeviceRemaining() diff --git a/libs/core/core/hooks/use-msc-device-detached-effect.ts b/libs/core/core/hooks/use-msc-device-detached-effect.ts new file mode 100644 index 0000000000..734e204497 --- /dev/null +++ b/libs/core/core/hooks/use-msc-device-detached-effect.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { useCallback, useEffect } from "react" +import { useDispatch, useSelector } from "react-redux" +import { answerMain, useDebouncedEventsHandler } from "shared/utils" +import { + DeviceBaseProperties, + DeviceProtocolMainEvent, + DeviceType, +} from "device-protocol/models" +import { + abortMscFlashing, + FlashingProcessState, + selectFlashingProcessState, + selectIsFlashingInActivePhases, + setFlashingProcessState, + setMscFlashingInitialState, +} from "msc-flash-harmony" +import { Dispatch } from "Core/__deprecated__/renderer/store" + +export const useMscDeviceDetachedEffect = () => { + const handleDevicesDetached = useHandleDevicesDetached() + + const batchDeviceDetachedEvents = + useDebouncedEventsHandler(handleDevicesDetached) + + useEffect(() => { + return answerMain( + DeviceProtocolMainEvent.DeviceDetached, + batchDeviceDetachedEvents + ) + }, [batchDeviceDetachedEvents]) +} + +const useHandleDevicesDetached = () => { + const dispatch = useDispatch() + const flashingProcessState = useSelector(selectFlashingProcessState) + const flashingInActivePhases = useSelector(selectIsFlashingInActivePhases) + const flashingInWaitingForBackButtonState = + flashingProcessState === FlashingProcessState.WaitingForBackButton + const flashingInRestartingPhase = + flashingProcessState === FlashingProcessState.Restarting + + return useCallback( + (deviceDetachedEvents: DeviceBaseProperties[]) => { + const mscEvents = deviceDetachedEvents.filter( + ({ deviceType }) => deviceType === DeviceType.MuditaHarmonyMsc + ) + + if (mscEvents.length === 0) { + return + } + + if (flashingInWaitingForBackButtonState) { + dispatch(setMscFlashingInitialState()) + return + } + + if (flashingInRestartingPhase) { + dispatch(setFlashingProcessState(FlashingProcessState.Completed)) + } + + const reason = flashingInActivePhases + ? FlashingProcessState.Failed + : undefined + dispatch(abortMscFlashing({ reason })) + }, + [ + dispatch, + flashingInActivePhases, + flashingInWaitingForBackButtonState, + flashingInRestartingPhase, + ] + ) +} diff --git a/libs/core/core/styles/theming/theme.ts b/libs/core/core/styles/theming/theme.ts index 368c882a9d..0afb1dde30 100644 --- a/libs/core/core/styles/theming/theme.ts +++ b/libs/core/core/styles/theming/theme.ts @@ -26,6 +26,7 @@ const transparentBlack3 = "rgba(0, 0, 0, 0.3)" const red = "#e96a6a" const green = "#dfefde" const orange = "#FD9900" +const darkOrange = "#DD802A" const white = "#ffffff" @@ -78,6 +79,7 @@ const theme = { tetheringSeparator: blue3, deviceListSeparator: grey5, deviceListSeparatorHover: grey4, + warning: darkOrange, }, boxShadow: { full: transparentBlack2, diff --git a/libs/core/data-sync/actions/read-all-indexes.action.test.ts b/libs/core/data-sync/actions/read-all-indexes.action.test.ts index 3f4c4c930d..e598061622 100644 --- a/libs/core/data-sync/actions/read-all-indexes.action.test.ts +++ b/libs/core/data-sync/actions/read-all-indexes.action.test.ts @@ -42,7 +42,14 @@ describe("async `readAllIndexes` ", () => { expect(mockStore.getActions()).toEqual([ readAllIndexes.pending(requestId), readAllIndexes.fulfilled( - { contacts: {}, messages: {}, threads: {}, templates: {} }, + { + contacts: {}, + messages: {}, + threads: {}, + templates: {}, + callLog: {}, + alarms: {}, + }, requestId, undefined ), diff --git a/libs/core/data-sync/actions/read-all-indexes.action.ts b/libs/core/data-sync/actions/read-all-indexes.action.ts index 4a335065b3..35f04e6487 100644 --- a/libs/core/data-sync/actions/read-all-indexes.action.ts +++ b/libs/core/data-sync/actions/read-all-indexes.action.ts @@ -11,27 +11,25 @@ import { DataSyncEvent, } from "Core/data-sync/constants" import { getIndexRequest } from "Core/data-sync/requests" -import { - ContactObject, - MessageObject, - TemplateObject, - ThreadObject, -} from "Core/data-sync/types" import { AllIndexes } from "Core/data-sync/types/all-indexes.type" export const readAllIndexes = createAsyncThunk( DataSyncEvent.ReadAllIndexes, async (_, { rejectWithValue }) => { - const contacts = await getIndexRequest(DataIndex.Contact) - const messages = await getIndexRequest(DataIndex.Message) - const templates = await getIndexRequest(DataIndex.Template) - const threads = await getIndexRequest(DataIndex.Thread) + const contacts = await getIndexRequest(DataIndex.Contact) + const messages = await getIndexRequest(DataIndex.Message) + const templates = await getIndexRequest(DataIndex.Template) + const threads = await getIndexRequest(DataIndex.Thread) + const callLog = await getIndexRequest(DataIndex.CallLog) + const alarms = await getIndexRequest(DataIndex.Alarm) if ( contacts === undefined || messages === undefined || templates === undefined || - threads === undefined + threads === undefined || + callLog === undefined || + alarms === undefined ) { return rejectWithValue( new AppError(DataSyncError.ReadAllIndexes, "Read All Indexes fails") @@ -43,6 +41,8 @@ export const readAllIndexes = createAsyncThunk( messages: messages.documentStore.docs, templates: templates.documentStore.docs, threads: threads.documentStore.docs, + callLog: callLog.documentStore.docs, + alarms: alarms.documentStore.docs, } } ) diff --git a/libs/generic-view/store/src/lib/data-migration/data-migration-percentage-progress.interface.ts b/libs/core/data-sync/constants/call-log.constant.ts similarity index 55% rename from libs/generic-view/store/src/lib/data-migration/data-migration-percentage-progress.interface.ts rename to libs/core/data-sync/constants/call-log.constant.ts index 2778bb8fcf..1d3cf1c1f0 100644 --- a/libs/generic-view/store/src/lib/data-migration/data-migration-percentage-progress.interface.ts +++ b/libs/core/data-sync/constants/call-log.constant.ts @@ -3,9 +3,6 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export enum DataMigrationPercentageProgress { - None = 0, - CollectingData = 1, - TransferringData = 10, - Finished = 100, +export enum CallLogTable { + Calls = "calls", } diff --git a/libs/core/data-sync/constants/event.constant.ts b/libs/core/data-sync/constants/event.constant.ts new file mode 100644 index 0000000000..ff71880152 --- /dev/null +++ b/libs/core/data-sync/constants/event.constant.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum EventTable { + Alarms = "alarms", +} diff --git a/libs/core/data-sync/constants/index.constant.ts b/libs/core/data-sync/constants/index.constant.ts index f0b7ba7e36..2a1fe1b4fa 100644 --- a/libs/core/data-sync/constants/index.constant.ts +++ b/libs/core/data-sync/constants/index.constant.ts @@ -8,4 +8,6 @@ export enum DataIndex { Message = "message", Template = "template", Thread = "thread", + CallLog = "calllog", + Alarm = "alarm", } diff --git a/libs/core/data-sync/constants/index.ts b/libs/core/data-sync/constants/index.ts index e503d3a8c0..e801535808 100644 --- a/libs/core/data-sync/constants/index.ts +++ b/libs/core/data-sync/constants/index.ts @@ -3,6 +3,8 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +export * from "./event.constant" +export * from "./call-log.constant" export * from "./contact.constant" export * from "./controller.constant" export * from "./message.constant" diff --git a/libs/core/data-sync/controllers/data-sync.controller.ts b/libs/core/data-sync/controllers/data-sync.controller.ts index 025393bb2d..4fec9a10f1 100644 --- a/libs/core/data-sync/controllers/data-sync.controller.ts +++ b/libs/core/data-sync/controllers/data-sync.controller.ts @@ -3,12 +3,11 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { SerialisedIndexData } from "elasticlunr" import { IpcEvent } from "Core/core/decorators" import { IndexStorage } from "Core/index-storage/types" import { DataSyncService } from "Core/data-sync/services/data-sync.service" import { DataIndex, IpcDataSyncEvent } from "Core/data-sync/constants" -import { InitializeOptions } from "Core/data-sync/types" +import { GetIndex, InitializeOptions } from "Core/data-sync/types" export class DataSyncController { constructor( @@ -17,9 +16,7 @@ export class DataSyncController { ) {} @IpcEvent(IpcDataSyncEvent.GetIndex) - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public getIndex(indexName: DataIndex): SerialisedIndexData | undefined { + public getIndex(indexName: Name): GetIndex { const index = this.index.get(indexName) if (index === undefined) { diff --git a/libs/core/data-sync/indexes/alarm-indexer.ts b/libs/core/data-sync/indexes/alarm-indexer.ts new file mode 100644 index 0000000000..58cc5c821a --- /dev/null +++ b/libs/core/data-sync/indexes/alarm-indexer.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import path from "path" +import { Database } from "sql.js" +import { Index } from "elasticlunr" +import { ElasticlunrFactory } from "Core/index-storage/factories" +import { BaseIndexer } from "Core/data-sync/indexes/base.indexer" +import { EventTable } from "Core/data-sync/constants" +import { IndexerPresenter, AlarmObject, EventInput } from "Core/data-sync/types" +import { FileSystemService } from "Core/file-system/services/file-system.service.refactored" + +export class AlarmIndexer extends BaseIndexer { + constructor( + fileSystemService: FileSystemService, + private dataPresenter: IndexerPresenter + ) { + super(fileSystemService) + } + + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async index(fileDir: string, token?: string): Promise { + const db = await this.initTmpDatabase(fileDir, token) + const object = this.dataPresenter.serializeToObject(this.loadTables(db)) + return this.createIndex(object) + } + + private createIndex(data: AlarmObject[]): Index { + const index = ElasticlunrFactory.create() + + index.setRef("id") + index.addField("repeatDays") + index.addField("isEnabled") + index.addField("hour") + index.addField("minute") + index.addField("snoozeTime") + + data.forEach((item) => { + index.addDoc(item) + }) + + return index + } + + private loadTables(db: Database) { + return { + [EventTable.Alarms]: db.exec( + `SELECT _id, hour, minute, music_tone, enabled, snooze_duration, rrule FROM ${EventTable.Alarms};` + )[0] as unknown as EventInput["alarms"], + } + } + + private async initTmpDatabase( + fileDir: string, + token?: string + ): Promise { + const data = await this.getData(path.join(fileDir, "events.db"), token) + return new (await this.sql).Database(data) + } +} diff --git a/libs/core/data-sync/indexes/base.indexer.test.ts b/libs/core/data-sync/indexes/base.indexer.test.ts index 74030a978c..31713d7615 100644 --- a/libs/core/data-sync/indexes/base.indexer.test.ts +++ b/libs/core/data-sync/indexes/base.indexer.test.ts @@ -10,7 +10,9 @@ import { FileSystemService } from "Core/file-system/services/file-system.service jest.mock("fs") -class Indexer extends BaseIndexer {} +class Indexer extends BaseIndexer { + index = jest.fn() +} const json: DirectoryJSON = { "sync/index.db": "", diff --git a/libs/core/data-sync/indexes/base.indexer.ts b/libs/core/data-sync/indexes/base.indexer.ts index 64777af700..cb21a953dc 100644 --- a/libs/core/data-sync/indexes/base.indexer.ts +++ b/libs/core/data-sync/indexes/base.indexer.ts @@ -3,6 +3,7 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +import { Index } from "elasticlunr" import initSqlJs, { SqlJsStatic } from "sql.js" import { FileSystemService } from "Core/file-system/services/file-system.service.refactored" @@ -21,14 +22,17 @@ export abstract class BaseIndexer { }) } - public async getData( - filePath: string, - token: string - ): Promise { + public async getData(filePath: string, token?: string) { try { - return this.fileSystemService.readEncryptedFile(filePath, token) + return token + ? this.fileSystemService.readEncryptedFile(filePath, token) + : this.fileSystemService.readFile(filePath) } catch { return null } } + + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + abstract index(fileDir: string, token?: string): Promise> } diff --git a/libs/core/data-sync/indexes/call-log.indexer.ts b/libs/core/data-sync/indexes/call-log.indexer.ts new file mode 100644 index 0000000000..8a6d8f755c --- /dev/null +++ b/libs/core/data-sync/indexes/call-log.indexer.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import path from "path" +import { Database } from "sql.js" +import { Index } from "elasticlunr" +import { ElasticlunrFactory } from "Core/index-storage/factories" +import { BaseIndexer } from "Core/data-sync/indexes/base.indexer" +import { CallLogTable } from "Core/data-sync/constants" +import { + IndexerPresenter, + CallLogObject, + CallLogInput, +} from "Core/data-sync/types" +import { FileSystemService } from "Core/file-system/services/file-system.service.refactored" + +export class CallLogIndexer extends BaseIndexer { + constructor( + fileSystemService: FileSystemService, + private dataPresenter: IndexerPresenter + ) { + super(fileSystemService) + } + + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async index(fileDir: string, token?: string): Promise { + const db = await this.initTmpDatabase(fileDir, token) + const object = this.dataPresenter.serializeToObject(this.loadTables(db)) + return this.createIndex(object) + } + + private createIndex(data: CallLogObject[]): Index { + const index = ElasticlunrFactory.create() + + index.setRef("id") + index.addField("phone") + index.addField("callDate") + index.addField("callDuration") + index.addField("presentation") + index.addField("callType") + index.addField("isRead") + + data.forEach((item) => { + index.addDoc(item) + }) + + return index + } + + private loadTables(db: Database) { + return { + [CallLogTable.Calls]: db.exec( + `SELECT _id, number, e164number, presentation, date, duration, type, name, contactId, isRead FROM ${CallLogTable.Calls};` + )[0] as unknown as CallLogInput["calls"], + } + } + + private async initTmpDatabase( + fileDir: string, + token?: string + ): Promise { + const data = await this.getData(path.join(fileDir, "calllog.db"), token) + return new (await this.sql).Database(data) + } +} diff --git a/libs/core/data-sync/indexes/contact.indexer.ts b/libs/core/data-sync/indexes/contact.indexer.ts index d8ec983bfa..e0813d8d63 100644 --- a/libs/core/data-sync/indexes/contact.indexer.ts +++ b/libs/core/data-sync/indexes/contact.indexer.ts @@ -24,7 +24,7 @@ export class ContactIndexer extends BaseIndexer { super(fileSystemService) } - async index(fileDir: string, token: string): Promise> { + async index(fileDir: string, token?: string): Promise> { const db = await this.initTmpDatabase(fileDir, token) const object = this.dataPresenter.serializeToObject(this.loadTables(db)) return this.createIndex(object) @@ -75,7 +75,7 @@ export class ContactIndexer extends BaseIndexer { private async initTmpDatabase( fileDir: string, - token: string + token?: string ): Promise { const data = await this.getData(path.join(fileDir, "contacts.db"), token) return new (await this.sql).Database(data) diff --git a/libs/core/data-sync/indexes/index.ts b/libs/core/data-sync/indexes/index.ts index 6d4f15f0c1..27d75297bc 100644 --- a/libs/core/data-sync/indexes/index.ts +++ b/libs/core/data-sync/indexes/index.ts @@ -3,6 +3,8 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +export * from "./alarm-indexer" +export * from "./call-log.indexer" export * from "./contact.indexer" export * from "./message.indexer" export * from "./template.indexer" diff --git a/libs/core/data-sync/indexes/message.indexer.ts b/libs/core/data-sync/indexes/message.indexer.ts index 385fdc3f26..259b17ba15 100644 --- a/libs/core/data-sync/indexes/message.indexer.ts +++ b/libs/core/data-sync/indexes/message.indexer.ts @@ -24,7 +24,7 @@ export class MessageIndexer extends BaseIndexer { super(fileSystemService) } - async index(fileDir: string, token: string): Promise> { + async index(fileDir: string, token?: string): Promise> { const smsDb = await this.initTmpSmsDatabase(fileDir, token) const contactDb = await this.initTmpContactDatabase(fileDir, token) const object = this.dataPresenter.serializeToObject( @@ -66,7 +66,7 @@ export class MessageIndexer extends BaseIndexer { private async initTmpSmsDatabase( fileDir: string, - token: string + token?: string ): Promise { const data = await this.getData(path.join(fileDir, "sms.db"), token) return new (await this.sql).Database(data) @@ -74,7 +74,7 @@ export class MessageIndexer extends BaseIndexer { private async initTmpContactDatabase( fileDir: string, - token: string + token?: string ): Promise { const data = await this.getData(path.join(fileDir, "contacts.db"), token) return new (await this.sql).Database(data) diff --git a/libs/core/data-sync/indexes/template.indexer.ts b/libs/core/data-sync/indexes/template.indexer.ts index 12b86ebd9e..0fd0286f00 100644 --- a/libs/core/data-sync/indexes/template.indexer.ts +++ b/libs/core/data-sync/indexes/template.indexer.ts @@ -26,7 +26,7 @@ export class TemplateIndexer extends BaseIndexer { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-explicit-any - async index(fileDir: string, token: string): Promise { + async index(fileDir: string, token?: string): Promise { const db = await this.initTmpDatabase(fileDir, token) const object = this.dataPresenter.serializeToObject(this.loadTables(db)) return this.createIndex(object) @@ -57,7 +57,7 @@ export class TemplateIndexer extends BaseIndexer { private async initTmpDatabase( fileDir: string, - token: string + token?: string ): Promise { const data = await this.getData(path.join(fileDir, "sms.db"), token) return new (await this.sql).Database(data) diff --git a/libs/core/data-sync/indexes/thread.indexer.ts b/libs/core/data-sync/indexes/thread.indexer.ts index 4e186a1551..71c84518c2 100644 --- a/libs/core/data-sync/indexes/thread.indexer.ts +++ b/libs/core/data-sync/indexes/thread.indexer.ts @@ -24,7 +24,7 @@ export class ThreadIndexer extends BaseIndexer { super(fileSystemService) } - async index(fileDir: string, token: string): Promise> { + async index(fileDir: string, token?: string): Promise> { const smsDb = await this.initTmpSmsDatabase(fileDir, token) const contactDb = await this.initTmpContactDatabase(fileDir, token) const object = this.dataPresenter.serializeToObject( @@ -71,7 +71,7 @@ export class ThreadIndexer extends BaseIndexer { private async initTmpSmsDatabase( fileDir: string, - token: string + token?: string ): Promise { const data = await this.getData(path.join(fileDir, "sms.db"), token) return new (await this.sql).Database(data) @@ -79,7 +79,7 @@ export class ThreadIndexer extends BaseIndexer { private async initTmpContactDatabase( fileDir: string, - token: string + token?: string ): Promise { const data = await this.getData(path.join(fileDir, "contacts.db"), token) return new (await this.sql).Database(data) diff --git a/libs/core/data-sync/presenters/alarm/alarm-presenter.ts b/libs/core/data-sync/presenters/alarm/alarm-presenter.ts new file mode 100644 index 0000000000..c72cc5e99b --- /dev/null +++ b/libs/core/data-sync/presenters/alarm/alarm-presenter.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { AlarmEntity, EventInput, AlarmObject } from "Core/data-sync/types" +import { BasePresenter } from "Core/data-sync/presenters/base-presenter" +import { UnifiedAlarmRepeatDaysType } from "device/models" + +export class AlarmPresenter extends BasePresenter { + public serializeToObject(input: EventInput): AlarmObject[] { + if (input.alarms === undefined) { + return [] + } + + const alarms = this.serializeRecord( + input.alarms.values, + input.alarms.columns + ) + + return alarms.map((alarm) => { + return { + id: String(alarm._id), + repeatDays: this.parseRRule(alarm.rrule), + isEnabled: alarm.enabled === "1", + hour: this.parseNumber(alarm.hour), + minute: this.parseNumber(alarm.minute), + snoozeTime: this.parseNumber(alarm.snooze_duration), + } + }) + } + + private parseNumber(value: string | undefined | null): number { + return Number(value) || 0 + } + + private parseRRule(rrule: string): UnifiedAlarmRepeatDaysType { + const daysMap: Record = { + MO: 1, + TU: 2, + WE: 3, + TH: 4, + FR: 5, + SA: 6, + SU: 7, + } + const repeatDays: UnifiedAlarmRepeatDaysType = [] + + if (rrule?.includes("BYDAY")) { + const byDayPart = rrule + .split(";") + .find((part) => part.startsWith("BYDAY")) + if (byDayPart) { + const days = byDayPart.replace("BYDAY=", "").split(",") + days.forEach((day) => { + const mappedDay = daysMap[day] + if (mappedDay) { + repeatDays.push(mappedDay) + } + }) + } + } + + return repeatDays + } +} diff --git a/libs/core/data-sync/presenters/alarm/alarm.presenter.test.ts b/libs/core/data-sync/presenters/alarm/alarm.presenter.test.ts new file mode 100644 index 0000000000..9f860cf5fd --- /dev/null +++ b/libs/core/data-sync/presenters/alarm/alarm.presenter.test.ts @@ -0,0 +1,389 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { AlarmPresenter } from "./alarm-presenter" +import { AlarmObject, EventInput } from "Core/data-sync/types" + +const presenter = new AlarmPresenter() + +describe("AlarmPresenter - serializeToObject", () => { + describe("basic serialization", () => { + test("serializes an alarm with all fields defined", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [ + [ + "3", + "1", + "0", + "/system/assets/audio/alarm/alarm_cymbals.mp3", + "1", + "0", + "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU", + ], + ], + }, + } + + const expected = [ + { + id: "3", + repeatDays: [1, 2, 3, 4, 5, 6, 7], + isEnabled: true, + hour: 1, + minute: 0, + snoozeTime: 0, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + + test("handles multiple alarm entries", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [ + [ + "4", + "2", + "0", + "/system/assets/audio/alarm/alarm_cymbals.mp3", + "1", + "0", + "", + ], + [ + "5", + "3", + "0", + "/system/assets/audio/alarm/alarm_cymbals.mp3", + "1", + "0", + "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", + ], + ], + }, + } + + const expected = [ + { + id: "4", + repeatDays: [], + isEnabled: true, + hour: 2, + minute: 0, + snoozeTime: 0, + }, + { + id: "5", + repeatDays: [1, 2, 3, 4, 5], + isEnabled: true, + hour: 3, + minute: 0, + snoozeTime: 0, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + }) + + describe("isEnabled field", () => { + test("handles isEnabled true", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [["7", "5", "30", "", "1", "10", ""]], + }, + } + + const expected = [ + { + id: "7", + repeatDays: [], + isEnabled: true, + hour: 5, + minute: 30, + snoozeTime: 10, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + + test("handles isEnabled false", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [["8", "6", "45", "", "0", "15", ""]], + }, + } + + const expected = [ + { + id: "8", + repeatDays: [], + isEnabled: false, + hour: 6, + minute: 45, + snoozeTime: 15, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + + test("handles missing isEnabled value, defaults to false", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [["9", "7", "15", "", "", "5", ""]], + }, + } + + const expected = [ + { + id: "9", + repeatDays: [], + isEnabled: false, + hour: 7, + minute: 15, + snoozeTime: 5, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + }) + + describe("repeatDays field", () => { + test("handles empty repeatDays array", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [["10", "8", "0", "", "1", "0", ""]], + }, + } + + const expected = [ + { + id: "10", + repeatDays: [], + isEnabled: true, + hour: 8, + minute: 0, + snoozeTime: 0, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + + test("handles repeatDays with one value", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [["11", "9", "15", "", "1", "5", "FREQ=WEEKLY;BYDAY=MO"]], + }, + } + + const expected = [ + { + id: "11", + repeatDays: [1], + isEnabled: true, + hour: 9, + minute: 15, + snoozeTime: 5, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + + test("handles repeatDays with multiple values", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [ + ["12", "10", "30", "", "1", "10", "FREQ=WEEKLY;BYDAY=MO,WE,FR"], + ], + }, + } + + const expected = [ + { + id: "12", + repeatDays: [1, 3, 5], + isEnabled: true, + hour: 10, + minute: 30, + snoozeTime: 10, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + + test("handles repeatDays with all values", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [ + [ + "13", + "11", + "0", + "", + "1", + "20", + "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU", + ], + ], + }, + } + + const expected = [ + { + id: "13", + repeatDays: [1, 2, 3, 4, 5, 6, 7], + isEnabled: true, + hour: 11, + minute: 0, + snoozeTime: 20, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + + describe("handling missing or invalid data", () => { + test("returns an empty array when alarms are undefined", () => { + const input: EventInput = { + alarms: undefined, + } + + const expected: AlarmObject[] = [] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + + test("handles missing snooze_duration, defaults to 0", () => { + const input: EventInput = { + alarms: { + columns: [ + "_id", + "hour", + "minute", + "music_tone", + "enabled", + "snooze_duration", + "rrule", + ], + values: [["6", "4", "0", "", "1", "", ""]], + }, + } + + const expected = [ + { + id: "6", + repeatDays: [], + isEnabled: true, + hour: 4, + minute: 0, + snoozeTime: 0, + }, + ] + + const result = presenter.serializeToObject(input) + expect(result).toEqual(expected) + }) + }) + }) +}) diff --git a/libs/core/data-sync/presenters/base-presenter.test.ts b/libs/core/data-sync/presenters/base-presenter.test.ts new file mode 100644 index 0000000000..c0afc80ee4 --- /dev/null +++ b/libs/core/data-sync/presenters/base-presenter.test.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { BasePresenter } from "Core/data-sync/presenters/base-presenter" + +class TestPresenter extends BasePresenter { + public serializeRecord(values: string[][], columns: string[]): Type[] { + return super.serializeRecord(values, columns) + } +} + +const presenter = new TestPresenter() + +test("`serializeRecord` serialize record properly", async () => { + const contactNameEntity = { + _id: "4", + contact_id: "4", + name_primary: "Theron", + name_alternative: "Paucek", + } + + const values: string[][] = [["4", "4", "Theron", "Paucek"]] + + const columns: string[] = [ + "_id", + "contact_id", + "name_primary", + "name_alternative", + ] + + const records = presenter.serializeRecord(values, columns) + expect(records).toHaveLength(1) + expect(records).toEqual([contactNameEntity]) +}) diff --git a/libs/core/data-sync/presenters/base-presenter.ts b/libs/core/data-sync/presenters/base-presenter.ts new file mode 100644 index 0000000000..e82becdd94 --- /dev/null +++ b/libs/core/data-sync/presenters/base-presenter.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export abstract class BasePresenter{ + protected serializeRecord(values: string[][], columns: string[]): Type[] { + return values.map((item) => { + return columns.reduce((acc: Record, value, index) => { + acc[value.trim()] = String(item[index]).trim() + return acc + }, {}) + }) as unknown as Type[] + } +} diff --git a/libs/core/data-sync/presenters/call-log/call-log.presenter.test.ts b/libs/core/data-sync/presenters/call-log/call-log.presenter.test.ts new file mode 100644 index 0000000000..797e0d947e --- /dev/null +++ b/libs/core/data-sync/presenters/call-log/call-log.presenter.test.ts @@ -0,0 +1,764 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { CallLogInput, CallLogObject } from "Core/data-sync/types" +import { CallLogPresenter } from "./call-log.presenter" + +const presenter = new CallLogPresenter() + +describe("CallLogPresenter - serializeToObject", () => { + describe("basic serialization", () => { + test("serializes a call log with all fields defined", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + [ + "1", + "123456789", + "+48123456789", + "1633036800", + "180", + "1", + "0", + "2", + ], + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "1", + phone: "+48123456789", + callDate: 1633036800000, + callDuration: 180, + callType: "TYPE_INCOMING", + isRead: false, + presentation: "PRESENTATION_PAYPHONE", // Mapped from "2" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + + test("handles multiple call log entries", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + [ + "2", + "987654321", + "+4987654321", + "1633036800", + "120", + "2", + "1", + "0", + ], + ["3", "", "+49321321321", "1633036800", "90", "3", "0", "1"], + ["4", "555555555", "", "1633036800", "60", "99", "0", "3"], + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "2", + phone: "+4987654321", + callDate: 1633036800000, + callDuration: 120, + callType: "TYPE_OUTGOING", + isRead: true, + presentation: "PRESENTATION_UNKNOWN", // Mapped from "0" + }, + { + id: "3", + phone: "+49321321321", + callDate: 1633036800000, + callDuration: 90, + callType: "TYPE_MISSED", + isRead: false, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + { + id: "4", + phone: "555555555", + callDate: 1633036800000, + callDuration: 60, + callType: "TYPE_OTHER", // TYPE_OTHER for unknown types + isRead: false, + presentation: "PRESENTATION_RESTRICTED", // Mapped from "3" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + }) + + describe("phone field", () => { + test("uses `number` when `e164number` is empty", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [["2", "987654321", "", "1633036800", "120", "2", "1", "1"]], + }, + } + + const expected: CallLogObject[] = [ + { + id: "2", + phone: "987654321", + callDate: 1633036800000, + callDuration: 120, + callType: "TYPE_OUTGOING", + isRead: true, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + + test("sets default `phone` as empty string when both `number` and `e164number` are missing", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [["5", "", "", "1633036800", "0", "1", "1", "2"]], + }, + } + + const expected: CallLogObject[] = [ + { + id: "5", + phone: "", + callDate: 1633036800000, + callDuration: 0, + callType: "TYPE_INCOMING", + isRead: true, + presentation: "PRESENTATION_PAYPHONE", // Mapped from "2" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + }) + + describe("isRead mapping", () => { + test("correctly maps `isRead`", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + [ + "9", + "111111111", + "+48111111111", + "1633036800", + "60", + "1", + "0", + "1", + ], + [ + "10", + "222222222", + "+48222222222", + "1633036800", + "120", + "2", + "1", + "2", + ], + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "9", + phone: "+48111111111", + callDate: 1633036800000, + callDuration: 60, + callType: "TYPE_INCOMING", + isRead: false, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + { + id: "10", + phone: "+48222222222", + callDate: 1633036800000, + callDuration: 120, + callType: "TYPE_OUTGOING", + isRead: true, + presentation: "PRESENTATION_PAYPHONE", // Mapped from "2" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + + test("ignores invalid `isRead` values, defaults to 0", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + [ + "8", + "222222222", + "+48222222222", + "1633036800", + "15", + "1", + "invalid", + "3", + ], + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "8", + phone: "+48222222222", + callDate: 1633036800000, + callDuration: 15, + callType: "TYPE_INCOMING", + isRead: true, + presentation: "PRESENTATION_RESTRICTED", // Mapped from "3" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + }) + + describe("call type mapping", () => { + test("correctly maps all call types", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + [ + "11", + "111111111", + "+48111111111", + "1633036800", + "60", + "0", + "1", + "2", + ], // CT_NONE + [ + "12", + "222222222", + "+48222222222", + "1633036800", + "120", + "1", + "0", + "1", + ], // CT_INCOMING + [ + "13", + "333333333", + "+48333333333", + "1633036800", + "180", + "2", + "1", + "3", + ], // CT_OUTGOING + [ + "14", + "444444444", + "+48444444444", + "1633036800", + "90", + "3", + "0", + "1", + ], // CT_MISSED + [ + "15", + "555555555", + "+48555555555", + "1633036800", + "200", + "4", + "1", + "0", + ], // CT_VOICEMAIL + [ + "16", + "666666666", + "+48666666666", + "1633036800", + "30", + "5", + "0", + "2", + ], // CT_REJECTED + [ + "17", + "777777777", + "+48777777777", + "1633036800", + "45", + "6", + "1", + "1", + ], // CT_BLOCKED + [ + "18", + "888888888", + "+48888888888", + "1633036800", + "75", + "7", + "0", + "0", + ], // CT_ANSW_EXT + [ + "19", + "999999999", + "+48999999999", + "1633036800", + "150", + "99", + "1", + "2", + ], // Unknown type + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "11", + phone: "+48111111111", + callDate: 1633036800000, + callDuration: 60, + callType: "TYPE_OTHER", // CT_NONE maps to TYPE_OTHER + isRead: true, + presentation: "PRESENTATION_PAYPHONE", // Mapped from "2" + }, + { + id: "12", + phone: "+48222222222", + callDate: 1633036800000, + callDuration: 120, + callType: "TYPE_INCOMING", // CT_INCOMING + isRead: false, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + { + id: "13", + phone: "+48333333333", + callDate: 1633036800000, + callDuration: 180, + callType: "TYPE_OUTGOING", // CT_OUTGOING + isRead: true, + presentation: "PRESENTATION_RESTRICTED", // Mapped from "3" + }, + { + id: "14", + phone: "+48444444444", + callDate: 1633036800000, + callDuration: 90, + callType: "TYPE_MISSED", // CT_MISSED + isRead: false, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + { + id: "15", + phone: "+48555555555", + callDate: 1633036800000, + callDuration: 200, + callType: "TYPE_VOICEMAIL", // CT_VOICEMAIL + isRead: true, + presentation: "PRESENTATION_UNKNOWN", // Mapped from "0" + }, + { + id: "16", + phone: "+48666666666", + callDate: 1633036800000, + callDuration: 30, + callType: "TYPE_REJECTED", // CT_REJECTED + isRead: false, + presentation: "PRESENTATION_PAYPHONE", // Mapped from "2" + }, + { + id: "17", + phone: "+48777777777", + callDate: 1633036800000, + callDuration: 45, + callType: "TYPE_BLOCKED", // CT_BLOCKED + isRead: true, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + { + id: "18", + phone: "+48888888888", + callDate: 1633036800000, + callDuration: 75, + callType: "TYPE_ANSWERED_EXTERNALLY", // CT_ANSW_EXT + isRead: false, + presentation: "PRESENTATION_UNKNOWN", // Mapped from "0" + }, + { + id: "19", + phone: "+48999999999", + callDate: 1633036800000, + callDuration: 150, + callType: "TYPE_OTHER", // Unknown type maps to TYPE_OTHER + isRead: true, + presentation: "PRESENTATION_PAYPHONE", // Mapped from "2" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + + test("sets default `callType` to 8 for unknown types", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + [ + "4", + "555555555", + "+49555555555", + "1633036800", + "60", + "99", + "0", + "1", + ], + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "4", + phone: "+49555555555", + callDate: 1633036800000, + callDuration: 60, + callType: "TYPE_OTHER", // TYPE_OTHER for unknown typ"TYPE_OUTGOINGs + isRead: false, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + }) + + describe("presentation type mapping", () => { + test("correctly maps all presentation types", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + [ + "2", + "987654321", + "+4987654321", + "1633036800", + "120", + "2", + "1", + "0", + ], + ["3", "", "+49321321321", "1633036800", "90", "3", "0", "1"], + ["4", "555555555", "", "1633036800", "60", "99", "0", "3"], + ["5", "666666666", "", "1633036800", "90", "1", "1", "4"], + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "2", + phone: "+4987654321", + callDate: 1633036800000, + callDuration: 120, + callType: "TYPE_OUTGOING", + isRead: true, + presentation: "PRESENTATION_UNKNOWN", // Mapped from "0" + }, + { + id: "3", + phone: "+49321321321", + callDate: 1633036800000, + callDuration: 90, + callType: "TYPE_MISSED", + isRead: false, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + { + id: "4", + phone: "555555555", + callDate: 1633036800000, + callDuration: 60, + callType: "TYPE_OTHER", // TYPE_OTHER for unknown types + isRead: false, + presentation: "PRESENTATION_RESTRICTED", // Mapped from "3" + }, + { + id: "5", + phone: "666666666", + callDate: 1633036800000, + callDuration: 90, + callType: "TYPE_INCOMING", + isRead: true, + presentation: "PRESENTATION_UNKNOWN", // Defaults to "3" for unknown presentation "4" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + + test("handles missing presentation values, defaults to `3`", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + [ + "6", + "999999999", + "+48999999999", + "1633036800", + "150", + "3", + "0", + "", + ], // presentation is empty + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "6", + phone: "+48999999999", + callDate: 1633036800000, + callDuration: 150, + callType: "TYPE_MISSED", + isRead: false, + presentation: "PRESENTATION_UNKNOWN", // Defaults to 3 when presentation is missing or empty + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + }) + + describe("handling missing or invalid data", () => { + test("sets `callDuration` to default value when duration is missing", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [ + ["2", "987654321", "+4987654321", "1633036800", "", "2", "1", "1"], + ], + }, + } + + const expected: CallLogObject[] = [ + { + id: "2", + phone: "+4987654321", + callDate: 1633036800000, + callDuration: 0, // Default duration when missing + callType: "TYPE_OUTGOING", + isRead: true, + presentation: "PRESENTATION_ALLOWED", // Mapped from "1" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + + test("sets default `callDate` when `date` is missing", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [["3", "321321321", "+49321321321", "", "90", "3", "0", "2"]], + }, + } + + const expected: CallLogObject[] = [ + { + id: "3", + phone: "+49321321321", + callDate: 0, // Default when date is missing + callDuration: 90, + callType: "TYPE_MISSED", + isRead: false, + presentation: "PRESENTATION_PAYPHONE", // Mapped from "2" + }, + ] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + + test("returns an empty array when there are no call log values", () => { + const callLogInput: CallLogInput = { + calls: { + columns: [ + "_id", + "number", + "e164number", + "date", + "duration", + "type", + "isRead", + "presentation", + ], + values: [], + }, + } + + const expected: CallLogObject[] = [] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + + test("returns an empty array when `calls` is undefined", () => { + const callLogInput: CallLogInput = { + calls: undefined, + } + + const expected: CallLogObject[] = [] + + const result = presenter.serializeToObject(callLogInput) + expect(result).toEqual(expected) + }) + }) +}) diff --git a/libs/core/data-sync/presenters/call-log/call-log.presenter.ts b/libs/core/data-sync/presenters/call-log/call-log.presenter.ts new file mode 100644 index 0000000000..5ab8333797 --- /dev/null +++ b/libs/core/data-sync/presenters/call-log/call-log.presenter.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { + CallLogEntity, + CallLogInput, + CallLogObject, +} from "Core/data-sync/types" +import { BasePresenter } from "Core/data-sync/presenters/base-presenter" +import { + UnifiedCallLogCallType, + UnifiedCallLogPresentationType, +} from "device/models" + +export class CallLogPresenter extends BasePresenter { + public serializeToObject(input: CallLogInput): CallLogObject[] { + if (input.calls === undefined) { + return [] + } + + const calls = this.serializeRecord( + input.calls.values, + input.calls.columns + ) + + return calls.map((call) => ({ + id: String(call._id), + phone: call.e164number || call.number || "", + callDate: this.parseNumber(call.date) * 1000, + callDuration: this.parseNumber(call.duration), + presentation: this.mapPresentationType(call.presentation), + callType: this.mapCallType(call.type), + isRead: this.mapIsNew(call.isRead), + })) + } + + private parseNumber(value: number | string | undefined | null): number { + return Number(value) || 0 + } + + private mapPresentationType( + type: number | string | undefined | null + ): UnifiedCallLogPresentationType { + const typesMap: Record = { + 0: "PRESENTATION_UNKNOWN", + 1: "PRESENTATION_ALLOWED", + 2: "PRESENTATION_PAYPHONE", + 3: "PRESENTATION_RESTRICTED", + } + return typesMap[Number(type)] ?? "PRESENTATION_UNKNOWN" + } + + private mapCallType( + type: number | string | undefined | null + ): UnifiedCallLogCallType { + const typesMap: Record = { + 0: "TYPE_OTHER", + 1: "TYPE_INCOMING", + 2: "TYPE_OUTGOING", + 3: "TYPE_MISSED", + 4: "TYPE_VOICEMAIL", + 5: "TYPE_REJECTED", + 6: "TYPE_BLOCKED", + 7: "TYPE_ANSWERED_EXTERNALLY", + } + return typesMap[Number(type)] ?? "TYPE_OTHER" + } + private mapIsNew(isRead: number | string | undefined | null): boolean { + return Number(isRead) !== 0 + } +} diff --git a/libs/core/data-sync/presenters/contact/contact.presenter.test.ts b/libs/core/data-sync/presenters/contact/contact.presenter.test.ts index 0a0874e0d0..33d9f88876 100644 --- a/libs/core/data-sync/presenters/contact/contact.presenter.test.ts +++ b/libs/core/data-sync/presenters/contact/contact.presenter.test.ts @@ -43,30 +43,6 @@ test("`findRecords` method return records by `contactId`", async () => { expect(records).toEqual([contactEntities[0]]) }) -// AUTO DISABLED - fix me if you like :) -// eslint-disable-next-line @typescript-eslint/require-await -test("`serializeRecord` serialize record properly", async () => { - const contactNameEntity: ContactNameEntity = { - _id: "4", - contact_id: "4", - name_primary: "Theron", - name_alternative: "Paucek", - } - - const values: string[][] = [["4", "4", "Theron", "Paucek"]] - - const columns: string[] = [ - "_id", - "contact_id", - "name_primary", - "name_alternative", - ] - - const records = presenter.serializeRecord(values, columns) - expect(records).toHaveLength(1) - expect(records).toEqual([contactNameEntity]) -}) - describe("when contact does not have any defined phone number", () => { const contactInput: ContactInput = { contacts: { diff --git a/libs/core/data-sync/presenters/contact/contact.presenter.ts b/libs/core/data-sync/presenters/contact/contact.presenter.ts index a967c9f47a..54bd7b80c6 100644 --- a/libs/core/data-sync/presenters/contact/contact.presenter.ts +++ b/libs/core/data-sync/presenters/contact/contact.presenter.ts @@ -13,10 +13,11 @@ import { ContactGroupEntity, ContactMatchGroupEntity, } from "Core/data-sync/types" +import { BasePresenter } from "Core/data-sync/presenters/base-presenter" const forbiddenRestrictedPureId = "0" -export class ContactPresenter { +export class ContactPresenter extends BasePresenter { public findRecords( data: { contact_id: string }[], contactId: string @@ -28,15 +29,6 @@ export class ContactPresenter { ) } - public serializeRecord(values: string[][], columns: string[]): Type[] { - return values.map((item) => { - return columns.reduce((acc: Record, value, index) => { - acc[value.trim()] = String(item[index]).trim() - return acc - }, {}) - }) as unknown as Type[] - } - private contactFavored( groups: ContactGroupEntity[], contactGroup?: ContactMatchGroupEntity diff --git a/libs/core/data-sync/presenters/index.ts b/libs/core/data-sync/presenters/index.ts index c0d39c7f96..ec19e69df6 100644 --- a/libs/core/data-sync/presenters/index.ts +++ b/libs/core/data-sync/presenters/index.ts @@ -3,6 +3,8 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +export * from "./alarm/alarm-presenter" +export * from "./call-log/call-log.presenter" export * from "./contact/contact.presenter" export * from "./message/message.presenter" export * from "./template/template.presenter" diff --git a/libs/core/data-sync/presenters/message/message.presenter.ts b/libs/core/data-sync/presenters/message/message.presenter.ts index 880c6692dc..c7f04763fb 100644 --- a/libs/core/data-sync/presenters/message/message.presenter.ts +++ b/libs/core/data-sync/presenters/message/message.presenter.ts @@ -12,8 +12,9 @@ import { ContactNumberEntity, ThreadEntity, } from "Core/data-sync/types" +import { BasePresenter } from "Core/data-sync/presenters/base-presenter" -export class MessagePresenter { +export class MessagePresenter extends BasePresenter { public findRecords( data: { _id: string }[], recordId: string @@ -21,15 +22,6 @@ export class MessagePresenter { return (data as unknown as Type[]).find((item) => item._id === recordId) } - public serializeRecord(values: string[][], columns: string[]): Type[] { - return values.map((item) => { - return columns.reduce((acc: Record, value, index) => { - acc[value.trim()] = String(item[index]).trim() - return acc - }, {}) - }) as unknown as Type[] - } - public serializeToObject(data: MessageInput): MessageObject[] { if (!data.sms || !data.contact_number || !data.threads) { return [] diff --git a/libs/core/data-sync/presenters/template/template.presenter.ts b/libs/core/data-sync/presenters/template/template.presenter.ts index 32bc67e140..37204f099d 100644 --- a/libs/core/data-sync/presenters/template/template.presenter.ts +++ b/libs/core/data-sync/presenters/template/template.presenter.ts @@ -8,8 +8,9 @@ import { TemplateInput, TemplateEntity, } from "Core/data-sync/types" +import { BasePresenter } from "Core/data-sync/presenters/base-presenter" -export class TemplatePresenter { +export class TemplatePresenter extends BasePresenter { public findRecords( data: { _id: string }[], recordId: string @@ -17,15 +18,6 @@ export class TemplatePresenter { return (data as unknown as Type[]).find((item) => item._id === recordId) } - public serializeRecord(values: string[][], columns: string[]): Type[] { - return values.map((item) => { - return columns.reduce((acc: Record, value, index) => { - acc[value.trim()] = String(item[index]).trim() - return acc - }, {}) - }) as unknown as Type[] - } - public serializeToObject(data: TemplateInput): TemplateObject[] { if (data.templates === undefined) { return [] diff --git a/libs/core/data-sync/presenters/thread/thread.presenter.ts b/libs/core/data-sync/presenters/thread/thread.presenter.ts index 03d9ecf41d..b2e6559ca7 100644 --- a/libs/core/data-sync/presenters/thread/thread.presenter.ts +++ b/libs/core/data-sync/presenters/thread/thread.presenter.ts @@ -13,8 +13,9 @@ import { } from "Core/data-sync/types" import { MessageType as PureMessageType } from "Core/device/constants" import { MessageType } from "Core/messages/constants" +import { BasePresenter } from "Core/data-sync/presenters/base-presenter" -export class ThreadPresenter { +export class ThreadPresenter extends BasePresenter { private findRecords( data: { _id: string }[], recordId: string @@ -36,15 +37,6 @@ export class ThreadPresenter { return data.filter((item) => item.contact_id === recordId).reverse()[0] } - private serializeRecord(values: string[][], columns: string[]): Type[] { - return values.map((item) => { - return columns.reduce((acc: Record, value, index) => { - acc[value.trim()] = String(item[index]).trim() - return acc - }, {}) - }) as unknown as Type[] - } - public serializeToObject(data: ThreadInput): ThreadObject[] { if (!data.threads || !data.contact_number) { return [] diff --git a/libs/core/data-sync/requests/get-index.request.ts b/libs/core/data-sync/requests/get-index.request.ts index c3d52d1ab8..0f12eacbf9 100644 --- a/libs/core/data-sync/requests/get-index.request.ts +++ b/libs/core/data-sync/requests/get-index.request.ts @@ -4,13 +4,11 @@ */ import { ipcRenderer } from "electron-better-ipc" -import { SerialisedIndexData } from "elasticlunr" -import { IpcDataSyncEvent, DataIndex } from "Core/data-sync/constants" +import { DataIndex, IpcDataSyncEvent } from "Core/data-sync/constants" +import { GetIndex } from "Core/data-sync/types" -// AUTO DISABLED - fix me if you like :) -// eslint-disable-next-line @typescript-eslint/ban-types -export const getIndexRequest = async ( - indexName: DataIndex -): Promise | undefined> => { +export const getIndexRequest = async ( + indexName: Name +): Promise> => { return ipcRenderer.callMain(IpcDataSyncEvent.GetIndex, indexName) } diff --git a/libs/core/data-sync/services/data-sync.service.ts b/libs/core/data-sync/services/data-sync.service.ts index d3008cca10..fad42da544 100644 --- a/libs/core/data-sync/services/data-sync.service.ts +++ b/libs/core/data-sync/services/data-sync.service.ts @@ -11,10 +11,12 @@ import { DataIndex } from "Core/index-storage/constants" import { DeviceProtocol } from "device-protocol/feature" import { MetadataStore } from "Core/metadata/services" import { + AlarmIndexer, + CallLogIndexer, ContactIndexer, MessageIndexer, - ThreadIndexer, TemplateIndexer, + ThreadIndexer, } from "Core/data-sync/indexes" import { FileSystemService } from "Core/file-system/services/file-system.service.refactored" import { @@ -22,15 +24,28 @@ import { MessagePresenter, TemplatePresenter, ThreadPresenter, + CallLogPresenter, + AlarmPresenter, } from "Core/data-sync/presenters" import { SyncBackupCreateService } from "Core/backup/services/sync-backup-create.service" import { InitializeOptions } from "Core/data-sync/types" +import { ElasticlunrFactory } from "Core/index-storage/factories" +import { BaseIndexer } from "Core/data-sync/indexes/base.indexer" + +const defaultRequiredIndexes: DataIndex[] = [ + DataIndex.Contact, + DataIndex.Message, + DataIndex.Template, + DataIndex.Thread, +] export class DataSyncService { private contactIndexer: ContactIndexer | null = null private messageIndexer: MessageIndexer | null = null private threadIndexer: ThreadIndexer | null = null private templateIndexer: TemplateIndexer | null = null + private callLogIndexer: CallLogIndexer | null = null + private alarmIndexer: AlarmIndexer | null = null private syncBackupCreateService: SyncBackupCreateService constructor( @@ -61,46 +76,80 @@ export class DataSyncService { this.fileSystemStorage, new TemplatePresenter() ) + this.callLogIndexer = new CallLogIndexer( + this.fileSystemStorage, + new CallLogPresenter() + ) + this.alarmIndexer = new AlarmIndexer( + this.fileSystemStorage, + new AlarmPresenter() + ) } public async indexAll({ token, serialNumber, + backupDirectory, + requiredIndexes = defaultRequiredIndexes, }: InitializeOptions): Promise { if ( !this.contactIndexer || !this.messageIndexer || !this.threadIndexer || - !this.templateIndexer + !this.templateIndexer || + !this.callLogIndexer || + !this.alarmIndexer ) { return false } - const syncFileDir = path.join(getAppPath(), "sync", serialNumber) - const { ok } = await this.syncBackupCreateService.createSyncBackup( - { - token, - extract: true, - cwd: syncFileDir, - }, - serialNumber - ) + const syncFileDir = + backupDirectory ?? path.join(getAppPath(), "sync", serialNumber) - if (!ok) { - return false + if (!backupDirectory) { + const { ok } = await this.syncBackupCreateService.createSyncBackup( + { + token, + extract: true, + cwd: syncFileDir, + }, + serialNumber + ) + + if (!ok) { + return false + } } - const contactIndex = await this.contactIndexer.index(syncFileDir, token) - const messageIndex = await this.messageIndexer.index(syncFileDir, token) - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const templateIndex = await this.templateIndexer.index(syncFileDir, token) - const threadIndex = await this.threadIndexer.index(syncFileDir, token) + const isRequired = (index: DataIndex) => requiredIndexes.includes(index) + + const indexers = { + [DataIndex.Contact]: this.contactIndexer, + [DataIndex.Message]: this.messageIndexer, + [DataIndex.Template]: this.templateIndexer, + [DataIndex.Thread]: this.threadIndexer, + [DataIndex.CallLog]: this.callLogIndexer, + [DataIndex.Alarm]: this.alarmIndexer, + } - this.index.set(DataIndex.Contact, contactIndex) - this.index.set(DataIndex.Message, messageIndex) - this.index.set(DataIndex.Template, templateIndex) - this.index.set(DataIndex.Thread, threadIndex) + const indexOrEmptyOnFailure = async ( + indexName: DataIndex, + indexer: BaseIndexer | null + ) => { + try { + return await indexer!.index(syncFileDir, token) + } catch (error) { + if (!isRequired(indexName)) { + return ElasticlunrFactory.create() + } + throw error + } + } + + for (const [indexName, indexer] of Object.entries(indexers)) { + const index = await indexOrEmptyOnFailure(indexName as DataIndex, indexer) + this.index.set(indexName as DataIndex, index) + } return true } diff --git a/libs/core/data-sync/types/alarm-object.type.ts b/libs/core/data-sync/types/alarm-object.type.ts new file mode 100644 index 0000000000..f15f5afebb --- /dev/null +++ b/libs/core/data-sync/types/alarm-object.type.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { UnifiedAlarm } from "device/models" +import { EventTable } from "Core/data-sync/constants" +import { Entity, DBQueryResult } from "Core/data-sync/types/entity.type" + +export type AlarmObject = UnifiedAlarm + +export type AlarmEntity = Entity<{ + _id: string + hour: string + minute: string + music_tone: string + enabled: string + snooze_duration: string + rrule: string +}> + +export interface EventInput { + [EventTable.Alarms]: DBQueryResult | undefined +} diff --git a/libs/core/data-sync/types/all-indexes.type.ts b/libs/core/data-sync/types/all-indexes.type.ts index 21e55b4956..4e5b7f3358 100644 --- a/libs/core/data-sync/types/all-indexes.type.ts +++ b/libs/core/data-sync/types/all-indexes.type.ts @@ -7,10 +7,14 @@ import { ContactObject } from "Core/data-sync/types/contact-object.type" import { MessageObject } from "Core/data-sync/types/message-object.type" import { TemplateObject } from "Core/data-sync/types/template-object.type" import { ThreadObject } from "Core/data-sync/types/thread-object.type" +import { CallLogObject } from "Core/data-sync/types/call-log-object.type" +import { AlarmObject } from "Core/data-sync/types/alarm-object.type" export interface AllIndexes { contacts: Record messages: Record templates: Record threads: Record + callLog: Record + alarms: Record } diff --git a/libs/core/data-sync/types/call-log-object.type.ts b/libs/core/data-sync/types/call-log-object.type.ts new file mode 100644 index 0000000000..f60327c3a9 --- /dev/null +++ b/libs/core/data-sync/types/call-log-object.type.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { UnifiedCallLog } from "device/models" +import { CallLogTable } from "Core/data-sync/constants" +import { Entity, DBQueryResult } from "Core/data-sync/types/entity.type" + +export type CallLogObject = UnifiedCallLog + +export type CallLogEntity = Entity<{ + _id: string + number: string + e164number: string + presentation: string + date: string + duration: string + type: string + name: string + contactId: string + isRead: string +}> + +export interface CallLogInput { + [CallLogTable.Calls]: + | DBQueryResult + | undefined +} diff --git a/libs/core/data-sync/types/get-index.type.ts b/libs/core/data-sync/types/get-index.type.ts new file mode 100644 index 0000000000..25e7d758cd --- /dev/null +++ b/libs/core/data-sync/types/get-index.type.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { SerialisedIndexData } from "elasticlunr" +import { ContactObject } from "./contact-object.type" +import { MessageObject } from "./message-object.type" +import { DataIndex } from "../constants/index.constant" +import { TemplateObject } from "./template-object.type" +import { ThreadObject } from "./thread-object.type" +import { CallLogObject } from "Core/data-sync/types/call-log-object.type" + +export type GetIndex = + | SerialisedIndexData< + Name extends DataIndex.Contact + ? ContactObject + : Name extends DataIndex.Message + ? MessageObject + : Name extends DataIndex.Template + ? TemplateObject + : Name extends DataIndex.Thread + ? ThreadObject + : Name extends DataIndex.CallLog + ? CallLogObject + : never + > + | undefined diff --git a/libs/core/data-sync/types/index.ts b/libs/core/data-sync/types/index.ts index e397204380..fd46ab650c 100644 --- a/libs/core/data-sync/types/index.ts +++ b/libs/core/data-sync/types/index.ts @@ -3,10 +3,13 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +export * from "./alarm-object.type" export * from "./all-indexes.type" +export * from "./call-log-object.type" export * from "./contact-object.type" export * from "./message-object.type" export * from "./thread-object.type" export * from "./presenter.type" export * from "./template-object.type" export { InitializeOptions } from "Core/data-sync/types/initialize-options.type" +export * from "./get-index.type" diff --git a/libs/core/data-sync/types/initialize-options.type.ts b/libs/core/data-sync/types/initialize-options.type.ts index f224096f17..808777f3a9 100644 --- a/libs/core/data-sync/types/initialize-options.type.ts +++ b/libs/core/data-sync/types/initialize-options.type.ts @@ -3,7 +3,11 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +import { DataIndex } from "Core/index-storage/constants" + export interface InitializeOptions { - token: string + token?: string serialNumber: string + backupDirectory?: string + requiredIndexes?: DataIndex[] } diff --git a/libs/core/device-file-system/commands/retrieve-files.command.test.ts b/libs/core/device-file-system/commands/retrieve-files.command.test.ts index 74d1bf35d4..33d590760c 100644 --- a/libs/core/device-file-system/commands/retrieve-files.command.test.ts +++ b/libs/core/device-file-system/commands/retrieve-files.command.test.ts @@ -15,6 +15,7 @@ const deviceProtocol = { device: { request: jest.fn(), }, + request: jest.fn(), } as unknown as DeviceProtocol const subject = new RetrieveFilesCommand(deviceProtocol) @@ -46,9 +47,7 @@ beforeEach(() => { describe("When `DeviceManager.device.request` returns success response", () => { beforeEach(() => { - deviceProtocol.device.request = jest - .fn() - .mockResolvedValueOnce(successResponse) + deviceProtocol.request = jest.fn().mockResolvedValueOnce(successResponse) }) test("returns `ResultObject.success` with payload", async () => { @@ -56,7 +55,7 @@ describe("When `DeviceManager.device.request` returns success response", () => { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/unbound-method - expect(deviceProtocol.device.request).toHaveBeenCalledWith({ + expect(deviceProtocol.request).toHaveBeenCalledWith(undefined, { endpoint: Endpoint.FileSystem, method: Method.Get, body: { @@ -69,9 +68,7 @@ describe("When `DeviceManager.device.request` returns success response", () => { describe("When `DeviceManager.device.request` returns failed response", () => { beforeEach(() => { - deviceProtocol.device.request = jest - .fn() - .mockResolvedValueOnce(failedResponse) + deviceProtocol.request = jest.fn().mockResolvedValueOnce(failedResponse) }) test("returns `ResultObject.failed` with payload", async () => { @@ -79,7 +76,7 @@ describe("When `DeviceManager.device.request` returns failed response", () => { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/unbound-method - expect(deviceProtocol.device.request).toHaveBeenCalledWith({ + expect(deviceProtocol.request).toHaveBeenCalledWith(undefined, { endpoint: Endpoint.FileSystem, method: Method.Get, body: { diff --git a/libs/core/device-file-system/commands/retrieve-files.command.ts b/libs/core/device-file-system/commands/retrieve-files.command.ts index eb74f410e3..9e3d79a7d6 100644 --- a/libs/core/device-file-system/commands/retrieve-files.command.ts +++ b/libs/core/device-file-system/commands/retrieve-files.command.ts @@ -13,10 +13,12 @@ import { GetFileSystemDirectoryResponseBody } from "Core/device/types/mudita-os" export class RetrieveFilesCommand extends BaseCommand { public async exec( - directory: string + directory: string, + deviceId = this.deviceProtocol.device.id ): Promise | undefined>> { const result = - await this.deviceProtocol.device.request( + await this.deviceProtocol.request( + deviceId, { endpoint: Endpoint.FileSystem, method: Method.Get, diff --git a/libs/core/device-file-system/services/device-file-system.service.ts b/libs/core/device-file-system/services/device-file-system.service.ts index f72fedf7ef..ff6bf0240c 100644 --- a/libs/core/device-file-system/services/device-file-system.service.ts +++ b/libs/core/device-file-system/services/device-file-system.service.ts @@ -223,13 +223,14 @@ export class DeviceFileSystemService { } public async removeDeviceFile( - removeFile: string + removeFile: string, + deviceId = this.deviceProtocol.device.id ): Promise> { if (!removeFile) { return Result.failed(new AppError("", "")) } - const { ok } = await this.deviceProtocol.device.request({ + const { ok } = await this.deviceProtocol.request(deviceId, { endpoint: Endpoint.FileSystem, method: Method.Delete, body: { diff --git a/libs/core/device-initialization/actions/initialize-mudita-harmony.action.ts b/libs/core/device-initialization/actions/initialize-mudita-harmony.action.ts index bb088cd197..b80b87a67c 100644 --- a/libs/core/device-initialization/actions/initialize-mudita-harmony.action.ts +++ b/libs/core/device-initialization/actions/initialize-mudita-harmony.action.ts @@ -16,6 +16,7 @@ import { isActiveDeviceAttachedSelector } from "device-manager/feature" import { isActiveDeviceProcessingSelector } from "Core/device/selectors/is-active-device-processing.selector" import { getCrashDump } from "Core/crash-dump" import { checkForForceUpdateNeed } from "Core/update/actions" +import { getTime } from "Core/time-synchronization/actions/get-time.action" export const initializeMuditaHarmony = createAsyncThunk< DeviceInitializationStatus, @@ -37,6 +38,8 @@ export const initializeMuditaHarmony = createAsyncThunk< ) } + await dispatch(getTime()) + const activeDeviceProcessing = isActiveDeviceProcessingSelector(getState()) if (!activeDeviceProcessing) { diff --git a/libs/core/device-initialization/components/devices-initialization-modal-flows/api-device-initialization-modal-flow.tsx b/libs/core/device-initialization/components/devices-initialization-modal-flows/api-device-initialization-modal-flow.tsx index b684c93e80..5ac4a4f2da 100644 --- a/libs/core/device-initialization/components/devices-initialization-modal-flows/api-device-initialization-modal-flow.tsx +++ b/libs/core/device-initialization/components/devices-initialization-modal-flows/api-device-initialization-modal-flow.tsx @@ -24,7 +24,7 @@ import { import { activeDeviceIdSelector } from "active-device-registry/feature" import { Modal } from "generic-view/ui" import { GenericThemeProvider } from "generic-view/theme" -import { ButtonAction, IconType } from "generic-view/utils" +import { IconType } from "generic-view/utils" import { FunctionComponent } from "Core/core/types/function-component.interface" import { Dispatch, ReduxRootState } from "Core/__deprecated__/renderer/store" import { intl } from "Core/__deprecated__/renderer/utils/intl" @@ -37,6 +37,7 @@ import { import { setDeviceInitializationStatus } from "Core/device-initialization/actions/base.action" import { DeviceInitializationStatus } from "Core/device-initialization/reducers/device-initialization.interface" import { useDeactivateDeviceAndRedirect } from "Core/overview/components/overview-screens/pure-overview/use-deactivate-device-and-redirect.hook" +import { ButtonAction } from "generic-view/models" const messages = defineMessages({ lockedModalHeadline: { diff --git a/libs/core/device-initialization/components/passcode-modal/passcode-modal.component.tsx b/libs/core/device-initialization/components/passcode-modal/passcode-modal.component.tsx index b6a64af321..a711cadaa8 100644 --- a/libs/core/device-initialization/components/passcode-modal/passcode-modal.component.tsx +++ b/libs/core/device-initialization/components/passcode-modal/passcode-modal.component.tsx @@ -11,9 +11,10 @@ import { AppError } from "Core/core/errors" import { ModalDialogProps } from "Core/ui" import { ModalLayers } from "Core/modals-manager/constants/modal-layers.enum" import { useHelpShortcut } from "help/store" +import { UnlockStatus } from "Core/device" export type UnlockDeviceReturnType = Promise< - PayloadAction + PayloadAction > interface Props extends Omit { @@ -79,7 +80,7 @@ const PasscodeModal: FunctionComponent = ({ return } - if (!unlockDeviceStatus.payload) { + if (unlockDeviceStatus.payload !== "UNLOCKED") { setErrorState(ErrorState.BadPasscode) return } diff --git a/libs/core/device-manager/services/usb-devices/usb-devices-mac.helper.test.ts b/libs/core/device-manager/services/usb-devices/usb-devices-mac.helper.test.ts deleted file mode 100644 index ebdbcb5926..0000000000 --- a/libs/core/device-manager/services/usb-devices/usb-devices-mac.helper.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { getHarmonyMSCDevice, parseToPortInfo } from "./usb-devices-mac.helper" -import { PortInfo } from "serialport" - -describe("getHarmonyMSCDevice", () => { - it("should return the correct device details", () => { - const output = ` - Mudita Harmony (MSC mode): - Product ID: 0x0103 - Vendor ID: 0x3310 - Version: 1.01 - Serial Number: 0123456789ABCDEF - Speed: Up to 480 Mb/s - Manufacturer: Mudita - Location ID: 0x00140000 / 4 - Current Available (mA): 500 - Current Required (mA): 100 - Extra Operating Current (mA): 0 - Mudita Pure: - Product ID: 0x0102 - Vendor ID: 0x3310 - Version: 1.12 - Serial Number: 25878580214921 - Speed: Up to 480 Mb/s - Manufacturer: Mudita - Location ID: 0x00120000 / 5 - Current Available (mA): 500 - Current Required (mA): 500 - Extra Operating Current (mA): 0 - ` - - const device = getHarmonyMSCDevice(output) - - expect(device).toEqual({ - name: "Mudita Harmony (MSC mode)", - VendorID: "0x3310", - ProductID: "0x0103", - SerialNumber: "0123456789ABCDEF", - Manufacturer: "Mudita", - LocationID: "0x00140000 / 4", - Version: "1.01", - Speed: "Up to 480 Mb/s", - "CurrentAvailable(mA)": "500", - "CurrentRequired(mA)": "100", - "ExtraOperatingCurrent(mA)": "0", - }) - }) - - it("should return null if no matching device is found", () => { - const output = ` - Mudita Pure: - Product ID: 0x0102 - Vendor ID: 0x3310 - Version: 1.12 - Serial Number: 25878580214921 - Speed: Up to 480 Mb/s - Manufacturer: Mudita - Location ID: 0x00120000 / 5 - Current Available (mA): 500 - Current Required (mA): 500 - Extra Operating Current (mA): 0 - ` - const device = getHarmonyMSCDevice(output) - expect(device).toBeNull() - }) -}) - -describe("parseToPortInfo", () => { - it("should parse DeviceDetails to PortInfo correctly", () => { - const deviceDetails = { - name: "Mudita Harmony (MSC mode)", - VendorID: "0x3310", - ProductID: "0x0103", - SerialNumber: "0123456789ABCDEF", - Manufacturer: "Mudita", - LocationID: "0x00140000 / 3", - } - - const portInfo: PortInfo = parseToPortInfo(deviceDetails) - - expect(portInfo).toEqual({ - path: "3310/0103/0123456789ABCDEF", - manufacturer: "Mudita", - serialNumber: "0123456789ABCDEF", - productId: "0103", - vendorId: "3310", - locationId: "0x00140000", - }) - }) -}) diff --git a/libs/core/device-manager/services/usb-devices/usb-devices-mac.helper.ts b/libs/core/device-manager/services/usb-devices/usb-devices-mac.helper.ts index 17014949fa..b46b5f86a7 100644 --- a/libs/core/device-manager/services/usb-devices/usb-devices-mac.helper.ts +++ b/libs/core/device-manager/services/usb-devices/usb-devices-mac.helper.ts @@ -3,86 +3,20 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ProductID, VendorID } from "Core/device/constants" -import { execPromise } from "shared/utils" import { PortInfo } from "serialport" - -interface DeviceDetails { - [key: string]: string | undefined - name?: string - ProductID?: string - VendorID?: string - Version?: string - SerialNumber?: string - Speed?: string - Manufacturer?: string - LocationID?: string - "CurrentAvailable(mA)"?: string - "CurrentRequired(mA)"?: string - "ExtraOperatingCurrent(mA)"?: string -} - -export const getHarmonyMSCDevice = (output: string): DeviceDetails | null => { - const devices: Array = [] - const lines = output.trim().split("\n") - let currentDevice: DeviceDetails = {} - let currentKey = "" - - lines.forEach((line) => { - if (line.trim() && !line.trim().endsWith(":")) { - const [keyParts, ...valueParts] = line.trim().split(":") - const key = keyParts.replaceAll(" ", "") - const value = valueParts.join(":").trim() - currentDevice[key] = value - } else if (line.trim().endsWith(":")) { - if (Object.keys(currentDevice).length) { - devices.push({ name: currentKey, ...currentDevice }) - currentDevice = {} - } - currentKey = line.trim().slice(0, -1) - } - }) - - if (Object.keys(currentDevice).length) { - devices.push({ name: currentKey, ...currentDevice }) - } - - return ( - devices.find((device) => { - if (device.VendorID && device.ProductID) { - return ( - device.VendorID.includes(VendorID.MuditaHarmony) && - device.ProductID.includes(ProductID.MuditaHarmonyMsc) - ) - } - return null - }) || null - ) -} - -export const parseToPortInfo = (device: DeviceDetails): PortInfo => { - const vendorId = device.VendorID!.replace("0x", "") - const productId = device.ProductID!.replace("0x", "") - const serialNumber = device.SerialNumber - - return { - path: `${vendorId}/${productId}/${serialNumber}`, - manufacturer: device.Manufacturer, - serialNumber: device.SerialNumber, - productId, - vendorId, - locationId: device.LocationID?.split(" ")[0], - } -} +import { MacosUSBPortDeviceParser } from "shared/utils" +import { ProductID, VendorID } from "Core/device/constants" export const getUsbDevicesMacOS = async (): Promise => { try { - const stdout = await execPromise("system_profiler SPUSBDataType") - if (stdout) { - const harmonyDevice = getHarmonyMSCDevice(stdout) - if (harmonyDevice) { - return parseToPortInfo(harmonyDevice) - } + const devices = await MacosUSBPortDeviceParser.getUSBPortDevices({ + vendorId: VendorID.MuditaHarmony, + productId: ProductID.MuditaHarmonyMsc, + }) + const device = devices[0] + + if (device !== undefined) { + return device } } catch (error) { console.error(error) diff --git a/libs/core/device-select/selectors/is-select-device-drawer-open.selector.ts b/libs/core/device-select/selectors/is-select-device-drawer-open.selector.ts index 2d39476d54..9b064ee037 100644 --- a/libs/core/device-select/selectors/is-select-device-drawer-open.selector.ts +++ b/libs/core/device-select/selectors/is-select-device-drawer-open.selector.ts @@ -5,10 +5,14 @@ import { createSelector } from "@reduxjs/toolkit" import { deviceManagerState } from "device-manager/feature" +import { selectTimeSynchronizationStatus } from "Core/time-synchronization/selectors/time-synchronization-status.selector" export const isSelectDeviceDrawerOpenSelector = createSelector( deviceManagerState, - ({ selectDeviceDrawerOpen }): boolean => { - return selectDeviceDrawerOpen + selectTimeSynchronizationStatus, + ({ selectDeviceDrawerOpen }, timeSyncStatus): boolean => { + return ( + selectDeviceDrawerOpen && ["idle", undefined].includes(timeSyncStatus) + ) } ) diff --git a/libs/core/device/constants/response-status.constant.ts b/libs/core/device/constants/response-status.constant.ts index c428a5c7f0..f6e62fdfee 100644 --- a/libs/core/device/constants/response-status.constant.ts +++ b/libs/core/device/constants/response-status.constant.ts @@ -8,6 +8,7 @@ export enum ResponseStatus { Accepted = 202, Redirect = 303, NoContent = 204, + MultiResponse = 207, BadRequest = 400, NotFound = 404, PhoneLocked = 403, diff --git a/libs/core/device/selectors/device-state.selector.test.ts b/libs/core/device/selectors/device-state.selector.test.ts index 46fcbf3e03..ba6c750be3 100644 --- a/libs/core/device/selectors/device-state.selector.test.ts +++ b/libs/core/device/selectors/device-state.selector.test.ts @@ -7,6 +7,17 @@ import { ReduxRootState } from "Core/__deprecated__/renderer/store" import { initialState, deviceReducer } from "Core/device/reducers" import { deviceStateSelector } from "Core/device/selectors/device-state.selector" +jest.mock("history", () => ({ + createHashHistory: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + block: jest.fn(), + listen: jest.fn(), + location: { pathname: "", search: "", hash: "", state: null }, + })), +})) + describe("`deviceStateSelector` selector", () => { test("when initial state is set selector returns initial state", () => { const state = { diff --git a/libs/core/device/selectors/get-left-time.selector.test.ts b/libs/core/device/selectors/get-left-time.selector.test.ts index d512c74fb2..063b45782c 100644 --- a/libs/core/device/selectors/get-left-time.selector.test.ts +++ b/libs/core/device/selectors/get-left-time.selector.test.ts @@ -8,6 +8,17 @@ import { ReduxRootState } from "Core/__deprecated__/renderer/store" import { deviceReducer, initialState } from "Core/device" import { getLeftTimeSelector } from "Core/device/selectors/get-left-time.selector" +jest.mock("history", () => ({ + createHashHistory: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + block: jest.fn(), + listen: jest.fn(), + location: { pathname: "", search: "", hash: "", state: null }, + })), +})) + MockDate.set("2000-2-1") describe("`getLeftTimeSelector` selector", () => { diff --git a/libs/core/files-manager/actions/upload-file.action.ts b/libs/core/files-manager/actions/upload-file.action.ts index 5aa1bbd40a..42a9624bcf 100644 --- a/libs/core/files-manager/actions/upload-file.action.ts +++ b/libs/core/files-manager/actions/upload-file.action.ts @@ -58,7 +58,7 @@ export const uploadFile = createAsyncThunk< if (!openFileResult.ok || !openFileResult.data) { dispatch(setUploadBlocked(false)) - return rejectWithValue("no files to upload") + return } const filePaths = openFileResult.data diff --git a/libs/core/files-manager/files-manager.module.ts b/libs/core/files-manager/files-manager.module.ts index 4a7e26ea98..0357ff9807 100644 --- a/libs/core/files-manager/files-manager.module.ts +++ b/libs/core/files-manager/files-manager.module.ts @@ -42,6 +42,7 @@ export class FilesManagerModule extends BaseModule { ) const fileManagerService = new FileManagerService( + this.deviceProtocol, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-call new FileDeleteCommand(this.deviceProtocol), diff --git a/libs/core/files-manager/services/file-manager.service.test.ts b/libs/core/files-manager/services/file-manager.service.test.ts index 773d8f2922..0a7f942582 100644 --- a/libs/core/files-manager/services/file-manager.service.test.ts +++ b/libs/core/files-manager/services/file-manager.service.test.ts @@ -14,6 +14,7 @@ import { } from "Core/device-file-system/commands" import { DeviceFileSystemError } from "Core/device-file-system/constants" import { FileDeleteCommand } from "Core/device-file-system/commands/file-delete.command" +import { DeviceProtocol } from "device-protocol/feature" // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -32,7 +33,15 @@ const fileUploadCommand = { exec: jest.fn(), } as unknown as FileUploadCommand +const deviceProtocol = { + request: jest.fn(), + device: { + id: "abc123", + }, +} as unknown as DeviceProtocol + const subject = new FileManagerService( + deviceProtocol, fileDeleteCommand, retrieveFilesCommand, fileUploadCommand diff --git a/libs/core/files-manager/services/file-manager.service.ts b/libs/core/files-manager/services/file-manager.service.ts index 1d83238dbd..c38277a07a 100644 --- a/libs/core/files-manager/services/file-manager.service.ts +++ b/libs/core/files-manager/services/file-manager.service.ts @@ -14,21 +14,23 @@ import { } from "Core/device-file-system/commands" import { DeviceFileSystemError } from "Core/device-file-system/constants" import { FileDeleteCommand } from "Core/device-file-system/commands/file-delete.command" +import { DeviceProtocol } from "device-protocol/feature" export class FileManagerService { constructor( + protected deviceProtocol: DeviceProtocol, private fileDeleteCommand: FileDeleteCommand, private retrieveFilesCommand: RetrieveFilesCommand, private fileUploadCommand: FileUploadCommand ) {} - public async getDeviceFiles({ - directory, - filter, - }: GetFilesInput): Promise> { + public async getDeviceFiles( + { directory, filter }: GetFilesInput, + deviceId = this.deviceProtocol.device.id + ): Promise> { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result = await this.retrieveFilesCommand.exec(directory) + const result = await this.retrieveFilesCommand.exec(directory, deviceId) if (!result.ok || !result.data) { return Result.failed( diff --git a/libs/core/index-storage/constants/data-index.constant.ts b/libs/core/index-storage/constants/data-index.constant.ts index f0b7ba7e36..a465403ea4 100644 --- a/libs/core/index-storage/constants/data-index.constant.ts +++ b/libs/core/index-storage/constants/data-index.constant.ts @@ -4,8 +4,10 @@ */ export enum DataIndex { + Alarm = "alarm", Contact = "contact", Message = "message", Template = "template", Thread = "thread", + CallLog = "calllog", } diff --git a/libs/core/index-storage/services/index-storage.service.ts b/libs/core/index-storage/services/index-storage.service.ts index 9ec9fc05ee..8abaf84423 100644 --- a/libs/core/index-storage/services/index-storage.service.ts +++ b/libs/core/index-storage/services/index-storage.service.ts @@ -12,7 +12,7 @@ import { MetadataStore } from "Core/metadata/services" import { FileSystemService } from "Core/file-system/services/file-system.service.refactored" import { InitializeOptions } from "Core/data-sync/types" -const cacheFileNames: Record = { +const cacheFileNames: Partial> = { [DataIndex.Contact]: "contacts.json", [DataIndex.Message]: "messages.json", [DataIndex.Thread]: "threads.json", @@ -46,10 +46,9 @@ export class IndexStorageService { return } - const data = await this.fileSystemService.readEncryptedFile( - filePath, - token - ) + const data = token + ? await this.fileSystemService.readEncryptedFile(filePath, token) + : await this.fileSystemService.readFile(filePath) if (data === undefined) { resolve(false) @@ -79,11 +78,18 @@ export class IndexStorageService { for (const [indexName, fileName] of Object.entries(cacheFileNames)) { const data = this.index.get(indexName as DataIndex) - await this.fileSystemService.writeEncryptedFile( - this.getCacheFilePath(fileName, serialNumber), - Buffer.from(JSON.stringify(data)), - token - ) + if (token) { + await this.fileSystemService.writeEncryptedFile( + this.getCacheFilePath(fileName, serialNumber), + Buffer.from(JSON.stringify(data)), + token + ) + } else { + await this.fileSystemService.writeFile( + this.getCacheFilePath(fileName, serialNumber), + Buffer.from(JSON.stringify(data)) + ) + } } } diff --git a/libs/core/modals-manager/components/modals-manager.component.tsx b/libs/core/modals-manager/components/modals-manager.component.tsx index 2a0053019f..08e9365075 100644 --- a/libs/core/modals-manager/components/modals-manager.component.tsx +++ b/libs/core/modals-manager/components/modals-manager.component.tsx @@ -5,6 +5,7 @@ import React from "react" import { useSelector } from "react-redux" +import { FlashingErrorModal, RecoveryCompleteModal } from "msc-flash-harmony" import { FunctionComponent } from "Core/core/types/function-component.interface" import ContactSupportFlow from "Core/contact-support/containers/contact-support-flow.container" import { UpdateOsInterruptedFlowContainer } from "Core/update/components/update-os-interrupted-flow" @@ -25,6 +26,8 @@ const ModalsManager: FunctionComponent = () => { + + {appUpdateVisible && } ) diff --git a/libs/core/modals-manager/components/use-loader-skip-on-connect.hook.ts b/libs/core/modals-manager/components/use-loader-skip-on-connect.hook.ts index 4bf07d12ee..f16f226fa5 100644 --- a/libs/core/modals-manager/components/use-loader-skip-on-connect.hook.ts +++ b/libs/core/modals-manager/components/use-loader-skip-on-connect.hook.ts @@ -13,6 +13,7 @@ import { isInitializationDeviceInProgress } from "Core/device-initialization/sel import { isInitializationAppInProgress } from "Core/app-initialization/selectors/is-initialization-app-in-progress.selector" import { useNoNewDevicesDetectedHook } from "Core/discovery-device/hooks/use-no-new-devices-detected.hook" import { selectDialogOpenState } from "shared/app-state" +import { selectTimeSynchronizationStatus } from "Core/time-synchronization/selectors/time-synchronization-status.selector" export const CONNECTING_LOADER_MODAL_ID = "connecting-loader-modal" @@ -26,6 +27,7 @@ export const useLoaderSkipOnConnect = () => { ) const initializationAppInProgress = useSelector(isInitializationAppInProgress) const dialogOpen = useSelector(selectDialogOpenState) + const timeSyncStatus = useSelector(selectTimeSynchronizationStatus) return useCallback(() => { return ( @@ -35,7 +37,8 @@ export const useLoaderSkipOnConnect = () => { activeDeviceProcessing || checkIsAnyOtherModalPresent(CONNECTING_LOADER_MODAL_ID) || !noNewDevicesDetectedState || - dialogOpen + dialogOpen || + !["idle", undefined].includes(timeSyncStatus) ) }, [ history.location.pathname, @@ -44,5 +47,6 @@ export const useLoaderSkipOnConnect = () => { activeDeviceProcessing, noNewDevicesDetectedState, dialogOpen, + timeSyncStatus, ]) } diff --git a/libs/core/overview/components/overview-screens/harmony-overview/overview-content.component.tsx b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview-content.component.tsx similarity index 80% rename from libs/core/overview/components/overview-screens/harmony-overview/overview-content.component.tsx rename to libs/core/overview/components/overview-screens/harmony-overview/harmony-overview-content.component.tsx index 968d2eac3c..9b62666129 100644 --- a/libs/core/overview/components/overview-screens/harmony-overview/overview-content.component.tsx +++ b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview-content.component.tsx @@ -9,9 +9,10 @@ import { DeviceType } from "device-protocol/models" import { FunctionComponent } from "Core/core/types/function-component.interface" import { DeviceInfo, + OverviewHarmonyWrapper, StatusInfo, SystemInfo, - OverviewWrapper, + TimeSynchronizationInfo, } from "Core/overview/components/overview/overview.styles" interface OverviewProps { @@ -23,9 +24,10 @@ interface OverviewProps { readonly batteryLevel: number readonly serialNumber: string | undefined readonly caseColour?: CaseColour + readonly synchronizeTime: () => void } -const OverviewContent: FunctionComponent = ({ +const HarmonyOverviewContent: FunctionComponent = ({ batteryLevel, onUpdateCheck, onUpdateDownload, @@ -34,9 +36,10 @@ const OverviewContent: FunctionComponent = ({ osVersion, serialNumber, caseColour, + synchronizeTime, }) => { return ( - + = ({ onDownload={onUpdateDownload} onUpdate={onUpdateInstall} /> - + + ) } -export default OverviewContent +export default HarmonyOverviewContent diff --git a/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.interface.ts b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.interface.ts index e638f67d2e..f828742bc4 100644 --- a/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.interface.ts +++ b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.interface.ts @@ -44,5 +44,6 @@ export interface HarmonyOverviewProps { readonly abortDownload: () => void readonly forceUpdate: (releases: OsRelease[]) => void readonly closeForceUpdateFlow: () => void + readonly synchronizeTime: () => void readonly caseColour: CaseColour } diff --git a/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.test.tsx b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.test.tsx index ec2bc26ded..cc1b06c409 100644 --- a/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.test.tsx +++ b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.test.tsx @@ -15,6 +15,8 @@ import React, { ComponentProps } from "react" import { Provider } from "react-redux" import { CheckForUpdateState } from "Core/update/constants/check-for-update-state.constant" import { CaseColour } from "core-device/models" +import { TimeSynchronizationTestIds } from "Core/overview/components/time-synchronization/time-synchronization-ids.enum" +import { selectSynchronizedTime } from "Core/time-synchronization/selectors/synchronized-time.selector" jest.mock("Core/settings/store/schemas/generate-application-id", () => ({ generateApplicationId: () => "123", @@ -30,6 +32,15 @@ jest.mock("@electron/remote", () => ({ MenuItem: () => jest.fn(), })) +jest.mock( + "Core/time-synchronization/selectors/synchronized-time.selector", + () => { + return { + selectSynchronizedTime: jest.fn(() => undefined), + } + } +) + type Props = ComponentProps const defaultProps: Props = { @@ -57,6 +68,7 @@ const defaultProps: Props = { forceUpdate: jest.fn(), forceUpdateState: State.Initial, closeForceUpdateFlow: jest.fn(), + synchronizeTime: jest.fn(), } const render = (extraProps?: Partial) => { @@ -78,4 +90,32 @@ test("Renders Mudita harmony data", () => { expect(queryByTestId(StatusTestIds.NetworkName)).not.toBeInTheDocument() queryByText(intl.formatMessage({ id: "module.overview.statusHarmonyTitle" })) expect(getByTestId(SystemTestIds.OsVersion)).toHaveTextContent("1.0.0") + expect( + queryByTestId(TimeSynchronizationTestIds.SynchronizeButton) + ).not.toBeInTheDocument() +}) + +test("Renders time synchronization box when feature is available", () => { + const mockDate = new Date("2021-12-31T13:45:00Z") + ;(selectSynchronizedTime as unknown as jest.Mock).mockReturnValue(mockDate) + const { queryByTestId, getByText } = render() + + const time = Intl.DateTimeFormat(undefined, { + hour: "2-digit", + minute: "2-digit", + timeZone: "UTC", + }).format(mockDate) + + const date = Intl.DateTimeFormat(undefined, { + day: "2-digit", + month: "2-digit", + year: "numeric", + timeZone: "UTC", + }).format(mockDate) + + expect( + queryByTestId(TimeSynchronizationTestIds.SynchronizeButton) + ).toBeInTheDocument() + expect(getByText(date)).toBeInTheDocument() + expect(getByText(time)).toBeInTheDocument() }) diff --git a/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx index 7ddb5c240d..009b2e4097 100644 --- a/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx +++ b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx @@ -6,7 +6,7 @@ import { DeviceType } from "device-protocol/models" import { Feature, flags } from "Core/feature-flags" import { HarmonyOverviewProps } from "Core/overview/components/overview-screens/harmony-overview/harmony-overview.component.interface" -import OverviewContent from "Core/overview/components/overview-screens/harmony-overview/overview-content.component" +import HarmonyOverviewContent from "Core/overview/components/overview-screens/harmony-overview/harmony-overview-content.component" import { UpdateOsFlow } from "Core/overview/components/update-os-flow" import UpdatingForceModalFlow from "Core/overview/components/updating-force-modal-flow/updating-force-modal-flow.component" import { CheckForUpdateMode } from "Core/update/constants" @@ -44,6 +44,7 @@ export const HarmonyOverview: FunctionComponent = ({ forceUpdateState, closeForceUpdateFlow, caseColour, + synchronizeTime, }) => { const openHelpShortcut = useHelpShortcut() const genericDeviceErrorModalOpened = useSelector( @@ -136,7 +137,7 @@ export const HarmonyOverview: FunctionComponent = ({ )} )} - = ({ onUpdateDownload={openCheckForUpdateModal} serialNumber={serialNumber} caseColour={caseColour} + synchronizeTime={synchronizeTime} /> ) diff --git a/libs/core/overview/components/overview/overview.styles.tsx b/libs/core/overview/components/overview/overview.styles.tsx index 7c2710da81..c18572fa85 100644 --- a/libs/core/overview/components/overview/overview.styles.tsx +++ b/libs/core/overview/components/overview/overview.styles.tsx @@ -9,6 +9,7 @@ import Status from "Core/overview/components/status/status.component" import System from "Core/overview/components/system/system.component" import FilesManager from "Core/overview/components/files-manager/files-manager.component" import Backup from "Core/overview/components/backup/backup.component" +import TimeSynchronization from "../time-synchronization/time-synchronization.component" export const DeviceInfo = styled(DevicePreview)` grid-area: Device; @@ -25,6 +26,10 @@ export const BackupInfo = styled(Backup)` grid-area: Backup; ` +export const TimeSynchronizationInfo = styled(TimeSynchronization)` + grid-area: TimeSynchronization; +` + const overviewWrapperWithBackup = css` grid-template-rows: repeat(3, minmax(20.4rem, 1fr)); grid-template-areas: @@ -49,6 +54,14 @@ export const OverviewPureWrapper = styled(OverviewWrapper)` ${overviewWrapperWithBackup}; ` +export const OverviewHarmonyWrapper = styled(OverviewWrapper)` + grid-template-areas: + "Device Network" + "Device System" + "Device TimeSynchronization"; + grid-template-rows: repeat(3, 1fr); +` + export const FileManagerInfo = styled(FilesManager)` grid-area: FilesManager; display: none; /* TODO: Remove when feature becomes available */ diff --git a/libs/core/overview/components/overview/overview.test.tsx b/libs/core/overview/components/overview/overview.test.tsx index d4c63d8bbc..6ce6db58c6 100644 --- a/libs/core/overview/components/overview/overview.test.tsx +++ b/libs/core/overview/components/overview/overview.test.tsx @@ -150,6 +150,7 @@ const defaultProps: Props = { forceUpdate: jest.fn(), forceUpdateState: State.Initial, backupActionDisabled: false, + synchronizeTime: jest.fn(), } const render = (extraProps?: Partial) => { diff --git a/libs/core/overview/components/time-synchronization/time-synchronization-ids.enum.ts b/libs/core/overview/components/time-synchronization/time-synchronization-ids.enum.ts new file mode 100644 index 0000000000..ecf36146e4 --- /dev/null +++ b/libs/core/overview/components/time-synchronization/time-synchronization-ids.enum.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum TimeSynchronizationTestIds { + SynchronizeButton = "time-synchronization-synchronize-button", + Time = "time-synchronization-time", + Date = "time-synchronization-date", +} diff --git a/libs/core/overview/components/time-synchronization/time-synchronization.component.tsx b/libs/core/overview/components/time-synchronization/time-synchronization.component.tsx new file mode 100644 index 0000000000..2c2fb986cf --- /dev/null +++ b/libs/core/overview/components/time-synchronization/time-synchronization.component.tsx @@ -0,0 +1,234 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import Card, { + CardAction, + CardBody, + CardContent, + CardHeader, +} from "Core/overview/components/card.elements" +import Text, { + TextDisplayStyle, +} from "Core/__deprecated__/renderer/components/core/text/text.component" +import { FunctionComponent } from "Core/core/types/function-component.interface" +import React, { useEffect, useMemo, useRef } from "react" +import { defineMessages, FormattedMessage } from "react-intl" +import { DisplayStyle } from "Core/__deprecated__/renderer/components/core/button/button.config" +import ButtonComponent from "Core/__deprecated__/renderer/components/core/button/button.component" +import { useDispatch, useSelector } from "react-redux" +import { selectTimeSynchronizationStatus } from "Core/time-synchronization/selectors/time-synchronization-status.selector" +import { resetTimeSynchronizationStatus } from "Core/time-synchronization/actions/reset-time-synchronization-status" +import { intl } from "Core/__deprecated__/renderer/utils/intl" +import { ModalSize } from "Core/__deprecated__/renderer/components/core/modal/modal.interface" +import { ModalContent, ModalDialog, RoundIconWrapper } from "Core/ui" +import Icon from "Core/__deprecated__/renderer/components/core/icon/icon.component" +import { IconType } from "Core/__deprecated__/renderer/components/core/icon/icon-type" +import { Dispatch } from "Core/__deprecated__/renderer/store" +import styled from "styled-components" +import { getTime } from "Core/time-synchronization/actions/get-time.action" +import { selectSynchronizedTime } from "Core/time-synchronization/selectors/synchronized-time.selector" +import { TimeSynchronizationTestIds } from "./time-synchronization-ids.enum" + +const messages = defineMessages({ + timeSynchronizationTitle: { + id: "module.overview.timeSynchronizationTitle", + }, + timeSynchronizationDescription: { + id: "module.overview.timeSynchronizationDescription", + }, + timeSynchronizationButton: { + id: "module.overview.timeSynchronizationButton", + }, + timeSynchronizationProgressButton: { + id: "module.overview.timeSynchronizationProgressButton", + }, + timeSynchronizationSuccessButton: { + id: "module.overview.timeSynchronizationSuccessButton", + }, + timeSynchronizationFailedSubtitle: { + id: "module.overview.timeSynchronizationFailedSubtitle", + }, + timeSynchronizationFailedDescription: { + id: "module.overview.timeSynchronizationFailedDescription", + }, + timeSynchronizationCurrentTimeLabel: { + id: "module.overview.timeSynchronizationCurrentTimeLabel", + }, +}) + +interface Props { + onSynchronize?: () => void +} + +const TimeSynchronization: FunctionComponent = ({ + onSynchronize, + ...props +}) => { + const dispatch = useDispatch() + const status = useSelector(selectTimeSynchronizationStatus) + const time = useSelector(selectSynchronizedTime) + const syncTimeoutRef = useRef() + const getTimeoutRef = useRef() + const firstTimeSyncRef = useRef(false) + + const hourCycle = new Intl.DateTimeFormat(undefined, { + timeStyle: "long", + }).resolvedOptions().hourCycle + + const deviceTime = Intl.DateTimeFormat(undefined, { + hour: "2-digit", + minute: "2-digit", + timeZone: "UTC", + }).format(time) + + const deviceDate = Intl.DateTimeFormat(undefined, { + day: "2-digit", + month: "2-digit", + year: "numeric", + timeZone: "UTC", + }).format(time) + + const onModalClose = () => { + dispatch(resetTimeSynchronizationStatus()) + } + + const buttonMessage = useMemo(() => { + switch (status) { + case "loading": + return messages.timeSynchronizationProgressButton + case "success": + return messages.timeSynchronizationSuccessButton + default: + return messages.timeSynchronizationButton + } + }, [status]) + + useEffect(() => { + clearTimeout(syncTimeoutRef.current) + if (status === "success") { + syncTimeoutRef.current = setTimeout(() => { + dispatch(resetTimeSynchronizationStatus()) + }, 3000) + } + return () => { + clearTimeout(syncTimeoutRef.current) + } + }, [dispatch, status]) + + useEffect(() => { + if (!time) return + if (!firstTimeSyncRef.current) { + dispatch(getTime()) + firstTimeSyncRef.current = true + return + } + clearTimeout(getTimeoutRef.current) + + const deviceSeconds = new Date(time).getSeconds() + const timeout = Math.max(1, 60 - deviceSeconds) + 1 + + getTimeoutRef.current = setTimeout(() => { + dispatch(getTime()) + }, timeout * 1000) + + return () => { + clearTimeout(getTimeoutRef.current) + } + }, [dispatch, time]) + + if (!time) return null + + return ( + <> + + + + + + + + + + + + + {deviceDate} + + + + + + + + + + + + + + + + + + ) +} + +export default TimeSynchronization + +const ContentLabel = styled(Text)` + width: 100%; +` + +const Time = styled(Text)<{ $cycle12?: boolean }>` + width: ${({ $cycle12 }) => ($cycle12 ? "6.5rem" : "4.2rem")}; +` + +const Content = styled(CardContent)` + flex-direction: row; + flex-wrap: wrap; + row-gap: 0.4rem; + column-gap: 2rem; +` + +const ModalHeading = styled(Text)` + margin-bottom: 0.8rem; +` diff --git a/libs/core/overview/overview.container.tsx b/libs/core/overview/overview.container.tsx index 9d8eec781b..f91298283f 100644 --- a/libs/core/overview/overview.container.tsx +++ b/libs/core/overview/overview.container.tsx @@ -40,6 +40,7 @@ import { forceUpdate } from "Core/update/actions/force-update/force-update.actio import { CheckForUpdateState } from "Core/update/constants/check-for-update-state.constant" import { isDataSyncInProgressSelector } from "Core/data-sync/selectors/is-data-sync-in-progress.selector" import { deactivateDevice } from "device-manager/feature" +import { synchronizeTime } from "Core/time-synchronization/actions/synchronize-time.action" const mapStateToProps = (state: RootModel & ReduxRootState) => { return { @@ -137,6 +138,8 @@ const mapDispatchToProps = (dispatch: TmpDispatch) => ({ // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call forceUpdate: (releases: OsRelease[]) => dispatch(forceUpdate({ releases })), + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + synchronizeTime: () => dispatch(synchronizeTime()), }) export default connect(mapStateToProps, mapDispatchToProps)(Overview) diff --git a/libs/core/settings/actions/load-settings.action.ts b/libs/core/settings/actions/load-settings.action.ts index e31a1467bc..f8f4e9287b 100644 --- a/libs/core/settings/actions/load-settings.action.ts +++ b/libs/core/settings/actions/load-settings.action.ts @@ -12,6 +12,7 @@ import logger from "Core/__deprecated__/main/utils/logger" import { getConfiguration } from "Core/settings/requests" import packageInfo from "../../../../apps/mudita-center/package.json" import { ReduxRootState } from "Core/__deprecated__/renderer/store" +import getBaseVersion from "Core/utils/get-base-bersion" export const loadSettings = createAsyncThunk< void, @@ -23,9 +24,11 @@ export const loadSettings = createAsyncThunk< const configuration = await getConfiguration() try { + const packageInfoBaseVersion = getBaseVersion(packageInfo.version) as string + updateRequired = isVersionGreater( configuration.centerVersion, - packageInfo.version + packageInfoBaseVersion ) // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/libs/core/settings/reducers/settings.reducer.test.ts b/libs/core/settings/reducers/settings.reducer.test.ts index 88b0d630a5..63eecb2579 100644 --- a/libs/core/settings/reducers/settings.reducer.test.ts +++ b/libs/core/settings/reducers/settings.reducer.test.ts @@ -14,6 +14,17 @@ import { } from "Core/__deprecated__/renderer/store/helpers" import { SettingsState } from "Core/settings/reducers" +jest.mock("history", () => ({ + createHashHistory: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + block: jest.fn(), + listen: jest.fn(), + location: { pathname: "", search: "", hash: "", state: null }, + })), +})) + const settings: SettingsState = { applicationId: "app-Nr8uiSV7KmWxX3WOFqZPF7uB", osBackupLocation: `fake/path/pure/phone/backups/`, diff --git a/libs/core/settings/selectors/get-device-lowest-version.selector.test.ts b/libs/core/settings/selectors/get-device-lowest-version.selector.test.ts index 19c1eb265f..cb52636964 100644 --- a/libs/core/settings/selectors/get-device-lowest-version.selector.test.ts +++ b/libs/core/settings/selectors/get-device-lowest-version.selector.test.ts @@ -8,6 +8,17 @@ import { ReduxRootState } from "Core/__deprecated__/renderer/store" import { initialState } from "Core/settings/reducers" import { getDeviceLatestVersion } from "Core/settings/selectors/get-device-lowest-version.selector" +jest.mock("history", () => ({ + createHashHistory: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + block: jest.fn(), + listen: jest.fn(), + location: { pathname: "", search: "", hash: "", state: null }, + })), +})) + const defaultState = { device: { deviceType: null, diff --git a/libs/core/settings/services/configuration.service.test.ts b/libs/core/settings/services/configuration.service.test.ts index 7f8ae4b702..03d3fe3c1d 100644 --- a/libs/core/settings/services/configuration.service.test.ts +++ b/libs/core/settings/services/configuration.service.test.ts @@ -8,6 +8,17 @@ import axios from "axios" import { Configuration } from "Core/settings/dto" import { ConfigurationService } from "Core/settings/services/configuration.service" +jest.mock("history", () => ({ + createHashHistory: jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + block: jest.fn(), + listen: jest.fn(), + location: { pathname: "", search: "", hash: "", state: null }, + })), +})) + const createMockAdapter = (): MockAdapter => { return new MockAdapter(axios) } diff --git a/libs/core/settings/static/app-configuration.json b/libs/core/settings/static/app-configuration.json index 93d2062e87..56fe0f1dfc 100644 --- a/libs/core/settings/static/app-configuration.json +++ b/libs/core/settings/static/app-configuration.json @@ -1 +1,4 @@ -{"centerVersion": "2.3.1", "productVersions": {"MuditaPure": "1.12.0", "MuditaHarmony": "2.8.0"}} +{ + "centerVersion": "2.3.1", + "productVersions": { "MuditaPure": "1.12.0", "MuditaHarmony": "2.8.0" } +} diff --git a/libs/core/templates/components/template-form/template-form.component.tsx b/libs/core/templates/components/template-form/template-form.component.tsx index 5d9b274657..64d47e174f 100644 --- a/libs/core/templates/components/template-form/template-form.component.tsx +++ b/libs/core/templates/components/template-form/template-form.component.tsx @@ -96,7 +96,10 @@ export const TemplateForm: FunctionComponent = ({