From 718e2ef35696ac6324473cf5177a87b0373a7b31 Mon Sep 17 00:00:00 2001 From: Jan Kobersky <5406945+kober32@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:04:41 +0200 Subject: [PATCH] Cordova build actions (#200) --- .github/actions/prepare/action.yml | 21 + .github/workflows/build-library.yml | 20 + .github/workflows/build.yml | 74 -- .github/workflows/publish.yml | 32 - .github/workflows/test-ios.yml | 34 + gulpfile.js | 16 + package-lock.json | 11 + package.json | 5 +- scripts/build-library.sh | 135 ---- scripts/common-functions.sh | 351 --------- scripts/update-apps.sh | 95 --- test-listener.js | 66 ++ testapp-cordova/gulpfile.js | 37 +- testapp-cordova/package-lock.json | 8 +- testapp-cordova/package.json | 7 +- .../CDVWebViewEngine/CDVWebViewEngine.m | 667 ++++++++++++++++++ testapp-cordova/src/App.tsx | 103 +-- testapp-cordova/www/index.html | 2 +- testapp/package-lock.json | 19 +- testapp/package.json | 10 +- testapp/src/App.tsx | 87 +-- testapp/src/TestExecutor.ts | 150 ++++ 22 files changed, 1050 insertions(+), 900 deletions(-) create mode 100644 .github/actions/prepare/action.yml create mode 100644 .github/workflows/build-library.yml delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/test-ios.yml delete mode 100755 scripts/build-library.sh delete mode 100644 scripts/common-functions.sh delete mode 100755 scripts/update-apps.sh create mode 100644 test-listener.js create mode 100644 testapp-cordova/patch-files/platforms/ios/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m create mode 100644 testapp/src/TestExecutor.ts diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml new file mode 100644 index 0000000..c4e7f08 --- /dev/null +++ b/.github/actions/prepare/action.yml @@ -0,0 +1,21 @@ +name: Prepare PowerAuth JS environment + +inputs: + env-file: + description: Contents of the testapp/.env file + required: false + default: "empty" + +runs: + using: composite + steps: + - name: Use Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install dependencies + shell: bash + run: npm i + - name: Set .env file + shell: bash + run: echo -e "${{ inputs.env-file }}" > testapp/.env \ No newline at end of file diff --git a/.github/workflows/build-library.yml b/.github/workflows/build-library.yml new file mode 100644 index 0000000..f17d962 --- /dev/null +++ b/.github/workflows/build-library.yml @@ -0,0 +1,20 @@ +name: Build + +on: + push: + branches: + - develop + - release/* + pull_request: + +jobs: + build-library: + name: Build and Pack the Library + runs-on: macos-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Prepare environment + uses: ./.github/actions/prepare + - name: Library build + run: npm run build \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 1396bf9..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: build library - -on: - push: - branches: - - develop - - master - pull_request: - schedule: - - cron: '25 6 * * *' - -jobs: - build-android: - name: Build Android library - runs-on: macos-latest - steps: - - name: Checkout the repo - uses: actions/checkout@v4 - - name: Setup Java 17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Setup NPM tools - run: npm install -g pod-install && npm install -g react-native && npm install -g typescript - - name: Build native Android libraries - run: bash scripts/build-library.sh android - - build-ios: - name: Build iOS library - runs-on: macos-latest - steps: - - name: Checkout the repo - uses: actions/checkout@v4 - - name: Setup Java 17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Setup NPM tools - run: npm install -g pod-install && npm install -g react-native && npm install -g typescript - - name: Build native iOS libraries - run: bash scripts/build-library.sh ios - - build-tsc: - name: Compile typescript - runs-on: macos-latest - steps: - - name: Checkout the repo - uses: actions/checkout@v4 - - name: Setup Java 17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: 'gradle' - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Setup NPM tools - run: npm install -g pod-install && npm install -g react-native && npm install -g typescript - - name: Compile sources - run: bash scripts/build-library.sh tsc \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index cf860c6..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Release a new version - -on: - workflow_dispatch: - inputs: - version: - description: 'Version of the library' - required: true - command: - description: 'Library deploy command' - required: false - default: prepare push deploy -v2 --any-branch - confirmBranch: - description: 'Confirm release branch' - required: true - -jobs: - publish: - name: Publish - runs-on: ubuntu-latest - steps: - - name: Checkout the repo - uses: actions/checkout@v2 - - name: Make sure we're on the proper branch - run: | - [[ $GITHUB_REF == refs/heads/${{ github.event.inputs.confirmBranch }} ]] || exit 1 - - name: Publish the library - uses: wultra/library-deploy@develop - with: - script-parameters: ${{ github.event.inputs.version }} ${{ github.event.inputs.command }} - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test-ios.yml b/.github/workflows/test-ios.yml new file mode 100644 index 0000000..97ec002 --- /dev/null +++ b/.github/workflows/test-ios.yml @@ -0,0 +1,34 @@ +name: iOS Tests + +on: + push: + branches: + - develop + - release/* + pull_request: + +jobs: + test-cordova: + name: Test Cordova iOS + runs-on: macos-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Prepare environment + uses: ./.github/actions/prepare + with: + env-file: ${{ secrets.ENV_TEST_FILE }} + - name: Run Cordova iOS Tests + run: npm run buildAndRunCordovaIosTests + test-react-native: + name: Test React Native iOS + runs-on: macos-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + - name: Prepare environment + uses: ./.github/actions/prepare + with: + env-file: ${{ secrets.ENV_TEST_FILE }} + - name: Run React-Native iOS Tests + run: npm run buildAndRunReactIosTests \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 8e58a86..bb2b931 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,3 +1,19 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + // Dependencies const gulp = require("gulp"); // gulp itself const ts = require("gulp-typescript"); // to be able to compiles typescript diff --git a/package-lock.json b/package-lock.json index 0782885..c873850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "gulp-strip-import-export": "^1.0.0", "gulp-typescript": "^6.0.0-alpha.1", "lodash": "^4.17.21", + "pod-install": "^0.2.2", "react-native": "^0.73.4", "rimraf": "^6.0.1" }, @@ -8813,6 +8814,16 @@ "node": ">=0.10.0" } }, + "node_modules/pod-install": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pod-install/-/pod-install-0.2.2.tgz", + "integrity": "sha512-NgQpKiuWZo8mWU+SVxmrn+ARy9+fFYzW53ze6CDTo70u5Ie8AVSn7FqolDC/c7+N4/kQ1BldAnXEab6SNYA8xw==", + "dev": true, + "license": "MIT", + "bin": { + "pod-install": "build/index.js" + } + }, "node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", diff --git a/package.json b/package.json index e74c147..ac21b82 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "types": "lib/index.d.ts", "source": "src/index", "scripts": { - "build": "node node_modules/gulp/bin/gulp.js" + "build": "node node_modules/gulp/bin/gulp.js", + "buildAndRunCordovaIosTests": "npm run build && pushd testapp-cordova && npm run reinstallPlugin && npm run build && npm run ios && popd && node test-listener.js", + "buildAndRunReactIosTests": "npm run build && pushd testapp && npm run reinstallPlugin && npx pod-install && (npm run start & npm run ios) && popd && node test-listener.js" }, "files": [ "README.md", @@ -56,6 +58,7 @@ "gulp-strip-import-export": "^1.0.0", "gulp-typescript": "^6.0.0-alpha.1", "lodash": "^4.17.21", + "pod-install": "^0.2.2", "react-native": "^0.73.4", "rimraf": "^6.0.1" } diff --git a/scripts/build-library.sh b/scripts/build-library.sh deleted file mode 100755 index 377ad9b..0000000 --- a/scripts/build-library.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash - -TOP=$(dirname $0) -source "$TOP/common-functions.sh" -SRC="${TOP}/.." - -DO_TSC=0 -DO_ANDROID=0 -DO_IOS=0 - -if [ ! -z $1 ]; then - case $1 in - android) - DO_ANDROID=1 - ;; - ios) - DO_IOS=1 - ;; - tsc | typescript) - DO_TSC=1 - ;; - *) - FAILURE "Unknown build target $1" ;; - esac -fi - -if [[ $DO_TSC$DO_ANDROID$DO_IOS == '000' ]]; then - DO_TSC=1 - DO_ANDROID=1 - DO_IOS=1 -fi - -LOG_LINE -LOG 'Installing dependencies' -LOG_LINE - -export NPM_TOKEN="DUMMY" # dummy variable to silence npm error - -# jump to top-project-folder -PUSH_DIR "${SRC}" - -# instal npm dependencies -[[ -d "node_modules" ]] && rm -rf node_modules -npm i - -if [ x$DO_IOS == x1 ]; then - LOG_LINE - LOG 'Building iOS platform' - LOG_LINE - - npx pod-install - - PUSH_DIR ios - - LOG_LINE - LOG 'Compiling iOS Release' - LOG_LINE - - xcrun xcodebuild \ - -workspace "PowerAuth.xcworkspace" \ - -scheme "PowerAuth" \ - -configuration "Release" \ - -sdk "iphonesimulator" \ - -arch x86_64 \ - build - - LOG_LINE - LOG 'Compiling iOS Debug' - LOG_LINE - - xcrun xcodebuild \ - -workspace "PowerAuth.xcworkspace" \ - -scheme "PowerAuth" \ - -configuration "Debug" \ - -sdk "iphonesimulator" \ - -arch x86_64 \ - build - - POP_DIR - -fi # DO_IOS - -if [ x$DO_ANDROID == x1 ]; then - LOG_LINE - LOG 'Building Android platform' - LOG_LINE - - - PUSH_DIR android - - ./gradlew clean build - - POP_DIR - -fi # DO_ANDROID - -if [ x$DO_TSC == x1 ]; then - LOG_LINE - LOG 'Building library' - LOG_LINE - - tsc --build - - PUSH_DIR testapp - - LOG_LINE - LOG 'Updating testapp dependencies' - LOG_LINE - - "${TOP}/update-apps.sh" - - LOG_LINE - LOG 'Building testapp' - LOG_LINE - - tsc --build - - POP_DIR - -fi # DO_TSC - -POP_DIR - -# THIS IS TEMPORARY DISABLED - FOR SOME REASON, GIT STATUS IS DIFFERENT IN PODS THAN IN LOCAL ENV. -# check if the git status is clean (there should be no changs) -# if [ -z "$(git status --porcelain)" ]; then -# echo "Git status clean." -# else -# echo "ERROR: Git status is not clean." -# git status -# git diff -# exit 1 -# fi - -EXIT_SUCCESS \ No newline at end of file diff --git a/scripts/common-functions.sh b/scripts/common-functions.sh deleted file mode 100644 index 6d5370c..0000000 --- a/scripts/common-functions.sh +++ /dev/null @@ -1,351 +0,0 @@ -#!/bin/bash -############################################################################### -# Global scope: -# Sets default script processing to very paranoid mode -# and turns off command echoing. -# -# Defines -# $VERBOSE - as level of information prints -# 0 - disables logging to stdout -# 1 - default logging to stdout -# 2 - debug logging to stdout (depends on script) -# ----------------------------------------------------------------------------- -set -e -set +v -VERBOSE=1 -LAST_LOG_IS_LINE=0 -############################################################################### -# Self update function -# -# Why? -# - We have copy of this script in several repositiories, so it would be great -# to simply self update it from one central point -# How? -# - type: sh common-functions.sh selfupdate - # ----------------------------------------------------------------------------- -function __COMMON_FUNCTIONS_SELF_UPDATE -{ - local self=$0 - local backup=$self.backup - local remote="https://raw.githubusercontent.com/wultra/library-deploy/master/common-functions.sh" - LOG_LINE - LOG "This script is going to update itself:" - LOG " source : $remote" - LOG " dest : $self" - LOG_LINE - PROMPT_YES_FOR_CONTINUE - cp $self $backup - wget $remote -O $self - LOG_LINE - LOG "Update looks good. Now you can:" - LOG " - press CTRL+C to cancel next step" - LOG " - or type 'y' to remove backup file" - LOG_LINE - PROMPT_YES_FOR_CONTINUE "Would you like to remove backup file?" - rm $backup -} -# ----------------------------------------------------------------------------- -# FAILURE prints error to stderr and exits the script with error code 1 -# ----------------------------------------------------------------------------- -function FAILURE -{ - echo "$CMD: Error: $@" 1>&2 - exit 1 -} -# ----------------------------------------------------------------------------- -# WARNING prints warning to stderr -# ----------------------------------------------------------------------------- -function WARNING -{ - echo "$CMD: Warning: $@" 1>&2 - LAST_LOG_IS_LINE=0 -} -# ----------------------------------------------------------------------------- -# LOG -# Prints all parameters to stdout if VERBOSE is greater than 0 -# LOG_LINE -# prints dashed line to stdout if VERBOSE is greater than 0 -# Function also prevents that two lines will never be displayed subsequently -# DEBUG_LOG -# Prints all parameters to stdout if VERBOSE is greater than 1 -# EXIT_SUCCESS -# print dashed line and "Success" text and exit process with code 0 -# if -l parameter is provided, then always prints dashed line -# ----------------------------------------------------------------------------- -function LOG -{ - if [ $VERBOSE -gt 0 ]; then - echo "$CMD: $@" - LAST_LOG_IS_LINE=0 - fi -} -function LOG_LINE -{ - if [ $LAST_LOG_IS_LINE -eq 0 ] && [ $VERBOSE -gt 0 ]; then - echo "$CMD: -----------------------------------------------------------------------------" - LAST_LOG_IS_LINE=1 - fi -} -function DEBUG_LOG -{ - if [ $VERBOSE -gt 1 ]; then - echo "$CMD: $@" - LAST_LOG_IS_LINE=0 - fi -} -function EXIT_SUCCESS -{ - [[ x$1 == 'x-l' ]] && LAST_LOG_IS_LINE=0 - LOG_LINE - LOG "Success" - exit 0 -} -# ----------------------------------------------------------------------------- -# PROMPT_YES_FOR_CONTINUE asks user whether script should continue -# -# Parameters: -# - $@ optional prompt -# ----------------------------------------------------------------------------- -function PROMPT_YES_FOR_CONTINUE -{ - local prompt="$@" - local answer - if [ -z "$prompt" ]; then - prompt="Would you like to continue?" - fi - read -p "$prompt (type y or yes): " answer - case "$answer" in - y | yes | Yes | YES) - LAST_LOG_IS_LINE=0 - return - ;; - *) - FAILURE "Aborted by user." - ;; - esac -} -# ----------------------------------------------------------------------------- -# REQUIRE_COMMAND uses "which" buildin command to test existence of requested -# tool on the system. -# -# Parameters: -# - $1 - tool to test (for example fastlane, pod, etc...) -# ----------------------------------------------------------------------------- -function REQUIRE_COMMAND -{ - set +e - local tool=$1 - local path=`which $tool` - if [ -z $path ]; then - FAILURE "$tool: required command not found." - fi - set -e - DEBUG_LOG "$tool: found at $path" -} -# ----------------------------------------------------------------------------- -# REQUIRE_COMMAND_PATH is similar to REQUIRE_COMMAND, but on success, prints -# path to stdout. You can use this function to check tool and acquire path to -# variable: TOOL_PATH=$(REQUIRE_COMMAND_PATH tool) -# -# Parameters: -# - $1 - tool to test (for example fastlane, pod, etc...) -# ----------------------------------------------------------------------------- -function REQUIRE_COMMAND_PATH -{ - set +e - local tool=$1 - local path=`which $tool` - if [ -z $path ]; then - FAILURE "$tool: required command not found." - fi - set -e - echo $path -} -# ----------------------------------------------------------------------------- -# Validates "verbose" command line switch and adjusts VERBOSE global variable -# according to desired level -# ----------------------------------------------------------------------------- -function SET_VERBOSE_LEVEL_FROM_SWITCH -{ - case "$1" in - -v0) VERBOSE=0 ;; - -v1) VERBOSE=1 ;; - -v2) VERBOSE=2 ;; - *) FAILURE "Invalid verbose level $1" ;; - esac - UPDATE_VERBOSE_COMMANDS -} -# ----------------------------------------------------------------------------- -# Updates verbose switches for common commands. Function will create following -# global variables: -# - $MD = mkdir -p [-v] -# - $RM = rm -f [-v] -# - $CP = cp [-v] -# - $MV = mv [-v] -# ----------------------------------------------------------------------------- -function UPDATE_VERBOSE_COMMANDS -{ - if [ $VERBOSE -lt 2 ]; then - # No verbose - CP="cp" - RM="rm -f" - MD="mkdir -p" - MV="mv" - else - # verbose - CP="cp -v" - RM="rm -f -v" - MD="mkdir -p -v" - MV="mv -v" - fi -} -# ----------------------------------------------------------------------------- -# Validate if $1 as VERSION has valid format: x.y.z -# Also sets global VERSION to $1 if VERSION string is empty. -# ----------------------------------------------------------------------------- -function VALIDATE_AND_SET_VERSION_STRING -{ - if [ -z "$1" ]; then - FAILURE "Version string is empty" - fi - local rx='^([0-9]+\.){2}(\*|[0-9]+)$' - if [[ ! "$1" =~ $rx ]]; then - FAILURE "Version string is invalid: '$1'" - fi - if [ -z "$VERSION" ]; then - VERSION=$1 - DEBUG_LOG "Changing version to $VERSION" - else - FAILURE "Version string is already set to $VERSION" - fi -} -# ----------------------------------------------------------------------------- -# Loads shared credentials, like API keys & logins. The function performs -# lookup in following order: -# if LIME_CREDENTIALS == 1 then does nothing, credentials are loaded -# if file exists at ${LIME_CREDENTIALS_FILE}, then loads the file -# if file exists at ~/.lime/credentials, then loads the file -# if file exists at .lime-credentials, then loads the file -# ----------------------------------------------------------------------------- -function LOAD_API_CREDENTIALS -{ - if [ x${API_CREDENTIALS} == x1 ]; then - DEBUG_LOG "Credentials are already set." - elif [ ! -z "${API_CREDENTIALS_FILE}" ]; then - source "${API_CREDENTIALS_FILE}" - elif [ -f "${HOME}/.lime/credentials" ]; then - source "${HOME}/.lime/credentials" - elif [ -f ".lime-credentials" ]; then - source ".lime-credentials" - else - FAILURE "Unable to locate credentials file." - fi - if [ x${LIME_CREDENTIALS} != x1 ]; then - FAILURE "Credentials file must set LIME_CREDENTIALS variable to 1" - fi -} - -# ----------------------------------------------------------------------------- -# PUSH_DIR & POP_DIR functions works just like pushd & popd builtin commands, -# but doesn't print a current directory, unless the VERBOSE level is 2. -# ----------------------------------------------------------------------------- -function PUSH_DIR -{ - if [ $VERBOSE -gt 1 ]; then - pushd "$1" - else - pushd "$1" > /dev/null - fi -} -function POP_DIR -{ - if [ $VERBOSE -gt 1 ]; then - popd - else - popd > /dev/null - fi -} - -# ----------------------------------------------------------------------------- -# SHA256, SHA384, SHA512 calculates appropriate SHA hash for given file and -# prints the result hash to stdout. Example: $(SHA256 my-file.txt) -# -# Parameters: -# $1 - input file -# ----------------------------------------------------------------------------- -function SHA256 -{ - local HASH=( `shasum -a 256 "$1"` ) - echo ${HASH[0]} -} -function SHA384 -{ - local HASH=( `shasum -a 384 "$1"` ) - echo ${HASH[0]} -} -function SHA512 -{ - local HASH=( `shasum -a 512 "$1"` ) - echo ${HASH[0]} -} - -# ----------------------------------------------------------------------------- -# Prints Xcode version into stdout or -1 in case of error. -# Parameters: -# $1 - optional switch, can be: -# '--full' - prints a full version of Xcode (e.g. 11.7.1) -# '--split' - prints a full, space separated version of Xcode (e.g. 11 7 1) -# '--major' - prints only a major version (e.g. 11) -# otherwise prints first line from `xcodebuild -version` result -# ----------------------------------------------------------------------------- -function GET_XCODE_VERSION -{ - local xcodever=(`xcodebuild -version | grep ^Xcode`) - local ver=${xcodever[1]} - if [ -z "$ver" ]; then - echo -1 - return - fi - local ver_array=( ${ver//./ } ) - case $1 in - --full) echo $ver ;; - --split) echo ${ver_array[@]} ;; - --major) echo ${ver_array[0]} ;; - *) echo ${xcodever[*]} ;; - esac -} - -# ----------------------------------------------------------------------------- -# Prints value of property from Java property file into stdout. -# The format of file is: -# KEY1=VALUE1 -# KEY2=VALUE2 -# -# Parameters: -# $1 - property file -# $2 - property key to print -# ----------------------------------------------------------------------------- -function GET_PROPERTY -{ - grep "^$2=" "$1" | cut -d'=' -f2 -} - -############################################################################### -# Global scope -# Gets full path to current directory and exits with error when -# folder is not valid. -# -# Defines -# $CMD - as current command name -# $TOP - path to $CMD -# ----------------------------------------------------------------------------- -CMD=$(basename $0) -TOP="`( cd \"$TOP\" && pwd )`" -UPDATE_VERBOSE_COMMANDS -if [ -z "$TOP" ]; then - FAILURE "Current dir is not accessible." -fi - -if [ "$CMD" == "common-functions.sh" ] && [ "$1" == "selfupdate" ]; then - __COMMON_FUNCTIONS_SELF_UPDATE -fi diff --git a/scripts/update-apps.sh b/scripts/update-apps.sh deleted file mode 100755 index 8dd83ba..0000000 --- a/scripts/update-apps.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -TOP=$(dirname $0) -source "$TOP/common-functions.sh" -SRC="${TOP}/.." - -REQUIRE_COMMAND "pod" -REQUIRE_COMMAND "npm" - -LIB='react-native-powerauth-mobile-sdk' - -DO_TEST=1 # apply for testapp -DO_RUN=0 # start platform apps -DO_PODS=0 # do pod install in project - -case $1 in - test | testapp) - DO_TEST=1 ;; - run | -r) - DO_RUN=1 ;; - pods | --cp) - DO_PODS=1 ;; - *) - DO_TEST=1 ;; -esac - -function LIB_PACKAGE -{ - - PUSH_DIR "$SRC" - - LOG_LINE - LOG 'Compiling typescript' - LOG_LINE - - tsc -b - - LOG_LINE - LOG 'Building library archive' - LOG_LINE - - local file=( ${LIB}-*.tgz ) - [[ -f "$file" ]] && $RM ${LIB}-*.tgz - - npm pack - LIB_ARCHIVE=$(ls | grep ${LIB}-*.tgz) - [[ -z "$LIB_ARCHIVE" ]] && FAILURE "npm pack did not produce library archive" - - POP_DIR -} - -function UPDATE_DEPENDENCIES -{ - local APP_NAME=$1 - - LOG_LINE - LOG "Updating dependency in '${APP_NAME}' project" - LOG_LINE - - PUSH_DIR "${SRC}/${APP_NAME}" - - npm r $LIB - npm i ../$LIB_ARCHIVE - - POP_DIR - - if [ x$DO_PODS == x1 ]; then - LOG_LINE - LOG 'Updating CocoaPods...' - LOG_LINE - - PUSH_DIR "${SRC}/${APP_NAME}/ios" - pod install - POP_DIR - fi - - if [ x$DO_RUN == x1 ]; then - PUSH_DIR "${SRC}/${APP_NAME}" - - LOG_LINE - LOG 'Starting Android app...' - LOG_LINE - npm run android - - LOG_LINE - LOG 'Starting iOS app...' - LOG_LINE - npm run ios - - POP_DIR - fi -} - -LIB_PACKAGE -[[ x$DO_TEST == x1 ]] && UPDATE_DEPENDENCIES testapp -EXIT_SUCCESS \ No newline at end of file diff --git a/test-listener.js b/test-listener.js new file mode 100644 index 0000000..3d3df64 --- /dev/null +++ b/test-listener.js @@ -0,0 +1,66 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// #################### +// #### Test listener is a node.js script that is running a HTTP server that recieves data from test application. +// #### Based on the data, it can either exit with success or error to give a hint that test ended up as a success or a failure. +// #################### + +const http = require('http'); +const { exit } = require('process'); + +const port = 8083 +const maxDurationSeconds = 180 // Test aer usually taking 60-90 seconds + +console.log(`Starting test listener @ localhost:${port}`) +console.log(`If the tests won't finish in ${maxDurationSeconds} seconds, this script will exit with the error exit code.`) + +// Fail-safe in case the test won't finish so we don't wait indefinitely +setTimeout(() => { + console.log(`Tick tock, time for tests run out (${maxDurationSeconds} seconds). Exiting with the error code.`); + exit(1); +}, maxDurationSeconds * 1000); + +http.createServer(function (req, res) { + + // the app is sending the status report in the `reportStatus` endpoint. + const isStatusReport = req.url.endsWith("reportStatus"); + + let body = ''; + req.on('data', (chunk) => { + body += chunk; + }); + req.on('end', () => { + + console.log(body); // print whatever came from the server + + if (isStatusReport) { + const report = JSON.parse(body); + if (report.total == report.progress) { // all tests finished + if (report.failed == 0) { + console.log("Tests went OK - exiting with the success code.") + exit(0) + } else { + console.log("Some tests failed - exiting with the error code.") + exit(1) + } + } + } + + res.write('OK'); + res.end(); + }); + }).listen(port); diff --git a/testapp-cordova/gulpfile.js b/testapp-cordova/gulpfile.js index 49d1102..9ae95a8 100644 --- a/testapp-cordova/gulpfile.js +++ b/testapp-cordova/gulpfile.js @@ -1,3 +1,19 @@ +// +// Copyright 2024 Wultra s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + const gulp = require("gulp"); const replace = require('gulp-replace'); const { build } = require("esbuild"); @@ -31,15 +47,16 @@ class Platform { ` // parse environment configuration -const getEnvConfig = dotenv.parse(fs.readFileSync(`${rnTestAppDir}/.env`)) -const envConfig = `const EnvConfig = ${JSON.stringify(getEnvConfig)};` +const envConfig = dotenv.parse(fs.readFileSync(`${rnTestAppDir}/.env`)) +console.log(`Reading env config env with pa server ${envConfig.POWERAUTH_SERVER_URL} and enrollment server ${envConfig.ENROLLMENT_SERVER_URL}`) +const envConfigStr = `const EnvConfig = ${JSON.stringify(envConfig)};` const copyTestFiles = () => gulp - .src([`${rnTestAppDir}/src/testbed/**/**.ts`, `${rnTestAppDir}/src/Config.ts`, `${rnTestAppDir}/_tests/**/**.ts`], { base: rnTestAppDir }) + .src([`${rnTestAppDir}/src/testbed/**/**.ts`, `${rnTestAppDir}/src/Config.ts`, `${rnTestAppDir}/src/TestExecutor.ts`, `${rnTestAppDir}/_tests/**/**.ts`], { base: rnTestAppDir }) .pipe(replace(/import {[a-zA-Z }\n,]+from "react-native-powerauth-mobile-sdk";/g, '')) .pipe(replace('import { Platform } from "react-native";', platformClass)) - .pipe(replace('import { Config as EnvConfig } from "react-native-config";', envConfig)) + .pipe(replace('import { Config as EnvConfig } from "react-native-config";', envConfigStr)) .pipe(gulp.dest(tempDir)); const copyAppFiles = () => @@ -52,11 +69,18 @@ const compile = () => entryPoints: [`${tempDir}/src/App.tsx`], outfile: outFile, bundle: true, + target: "ios13", // minify: true // do not minify for easier debug }) // to make sure all files are copied in the proper place -const buildiOS = () => exec("cordova build ios") +const prepareIOS = () => exec("npx cordova prepare ios") + +// patch testapp files +const patchNativeFiles = () => + gulp + .src("patch-files/platforms/**/**", { base: "patch-files" }) + .pipe(gulp.dest(".")) gulp.task("default", gulp.series( cleanTemp, @@ -64,5 +88,6 @@ gulp.task("default", gulp.series( copyAppFiles, compile, cleanTemp, - buildiOS, + prepareIOS, + patchNativeFiles, )); \ No newline at end of file diff --git a/testapp-cordova/package-lock.json b/testapp-cordova/package-lock.json index 7e1a79e..9fdf0d3 100644 --- a/testapp-cordova/package-lock.json +++ b/testapp-cordova/package-lock.json @@ -18,7 +18,7 @@ "esbuild": "^0.23.1", "gulp": "^5.0.0", "gulp-replace": "^1.1.4", - "powerauth-js-test-client": "^0.2.9", + "powerauth-js-test-client": "^0.2.12", "rimraf": "^6.0.1" } }, @@ -2873,9 +2873,9 @@ } }, "node_modules/powerauth-js-test-client": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/powerauth-js-test-client/-/powerauth-js-test-client-0.2.9.tgz", - "integrity": "sha512-tam5DpQajZ/442WoXCU3tVNLSkJVBINrvsrvkhQ04qA5wRx/GrcMT4JT48Up7iSzvcfMv9ky2DmYdmIKZPD3jA==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/powerauth-js-test-client/-/powerauth-js-test-client-0.2.12.tgz", + "integrity": "sha512-PhYSVQU+oMPiwhDD5H8OGXMuU0eQ0RKqfAKlL7aCUNXvp3dKNF5ib1Y7jtrUxI1telpBVXONOX8IGLjLNLDNGA==", "dev": true, "license": "Apache 2.0", "dependencies": { diff --git a/testapp-cordova/package.json b/testapp-cordova/package.json index 2374861..500ffa0 100644 --- a/testapp-cordova/package.json +++ b/testapp-cordova/package.json @@ -5,8 +5,9 @@ "description": "A sample Cordova application that runs PowerAuth tests.", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "reinstallPlugin": "cordova plugin remove cordova-powerauth-mobile-sdk && cordova plugin add ../build/cdv", + "ios": "npx cordova run ios", + "installPlugin": "npx cordova plugin add ../build/cdv", + "reinstallPlugin": "(npx cordova plugin remove cordova-powerauth-mobile-sdk || true) && npm run installPlugin", "build": "node node_modules/gulp/bin/gulp.js" }, "keywords": [ @@ -24,7 +25,7 @@ "esbuild": "^0.23.1", "gulp": "^5.0.0", "gulp-replace": "^1.1.4", - "powerauth-js-test-client": "^0.2.9", + "powerauth-js-test-client": "^0.2.12", "rimraf": "^6.0.1" }, "cordova": { diff --git a/testapp-cordova/patch-files/platforms/ios/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m b/testapp-cordova/patch-files/platforms/ios/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m new file mode 100644 index 0000000..0d4a03f --- /dev/null +++ b/testapp-cordova/patch-files/platforms/ios/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m @@ -0,0 +1,667 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDVWebViewEngine.h" +#import "CDVWebViewUIDelegate.h" +#import +#import +#import + +#import + +#define CDV_BRIDGE_NAME @"cordova" +#define CDV_WKWEBVIEW_FILE_URL_LOAD_SELECTOR @"loadFileURL:allowingReadAccessToURL:" + +// START: FOLLOWING BLOCK IS TO ENABLE WEAK CORS. THIS SOLUTION DOES NOT WORK FOR PRODUCTION! + +void WKPreferencesSetWebSecurityEnabled(id, bool); + +@interface WDBFakeWebKitPointer: NSObject +@property (nonatomic) void* _apiObject; +@end +@implementation WDBFakeWebKitPointer +@end + +void WDBSetWebSecurityEnabled(WKPreferences* prefs, bool enabled) { + Ivar ivar = class_getInstanceVariable([WKPreferences class], "_preferences"); + void* realPreferences = (void*)(((uintptr_t)prefs) + ivar_getOffset(ivar)); + WDBFakeWebKitPointer* fake = [WDBFakeWebKitPointer new]; + fake._apiObject = realPreferences; + WKPreferencesSetWebSecurityEnabled(fake, enabled); +} + +// END + +@interface CDVWebViewWeakScriptMessageHandler : NSObject + +@property (nonatomic, weak, readonly) idscriptMessageHandler; + +- (instancetype)initWithScriptMessageHandler:(id)scriptMessageHandler; + +@end + + +@interface CDVWebViewEngine () + +@property (nonatomic, strong, readwrite) UIView* engineWebView; +@property (nonatomic, strong, readwrite) id uiDelegate; +@property (nonatomic, weak) id weakScriptMessageHandler; +@property (nonatomic, strong) CDVURLSchemeHandler * schemeHandler; +@property (nonatomic, readwrite) NSString *CDV_ASSETS_URL; +@property (nonatomic, readwrite) Boolean cdvIsFileScheme; +@property (nullable, nonatomic, strong, readwrite) WKWebViewConfiguration *configuration; + +@end + +// see forwardingTargetForSelector: selector comment for the reason for this pragma +#pragma clang diagnostic ignored "-Wprotocol" + +@implementation CDVWebViewEngine + +@synthesize engineWebView = _engineWebView; + +- (nullable instancetype)initWithFrame:(CGRect)frame configuration:(nullable WKWebViewConfiguration *)configuration +{ + self = [super init]; + if (self) { + if (NSClassFromString(@"WKWebView") == nil) { + return nil; + } + + configuration = [[WKWebViewConfiguration alloc] init]; + self.configuration = configuration; + self.engineWebView = configuration ? [[WKWebView alloc] initWithFrame:frame configuration:configuration] : [[WKWebView alloc] initWithFrame:frame]; + // enable weak cors for testing app - this does not work for production + WDBSetWebSecurityEnabled([configuration preferences], false); + } + + return self; +} + + +- (nullable instancetype)initWithFrame:(CGRect)frame +{ + return [self initWithFrame:frame configuration:nil]; +} + +- (WKWebViewConfiguration*) createConfigurationFromSettings:(NSDictionary*)settings +{ + WKWebViewConfiguration* configuration; + if (_configuration) { + configuration = _configuration; + } else { + configuration = [[WKWebViewConfiguration alloc] init]; + configuration.processPool = [[CDVWebViewProcessPoolFactory sharedFactory] sharedProcessPool]; + } + + if (settings == nil) { + return configuration; + } + + configuration.allowsInlineMediaPlayback = [settings cordovaBoolSettingForKey:@"AllowInlineMediaPlayback" defaultValue:NO]; + + // Set the media types that are required for user action for playback + WKAudiovisualMediaTypes mediaType = WKAudiovisualMediaTypeAll; // default + + // targetMediaType will always exist, either from user's "config.xml" or default ("defaults.xml"). + id targetMediaType = [settings cordovaSettingForKey:@"MediaTypesRequiringUserActionForPlayback"]; + if ([targetMediaType isEqualToString:@"none"]) { + mediaType = WKAudiovisualMediaTypeNone; + } else if ([targetMediaType isEqualToString:@"audio"]) { + mediaType = WKAudiovisualMediaTypeAudio; + } else if ([targetMediaType isEqualToString:@"video"]) { + mediaType = WKAudiovisualMediaTypeVideo; + } else if ([targetMediaType isEqualToString:@"all"]) { + mediaType = WKAudiovisualMediaTypeAll; + } else { + NSLog(@"Invalid \"MediaTypesRequiringUserActionForPlayback\" was detected. Fallback to default value of \"all\" types."); + } + configuration.mediaTypesRequiringUserActionForPlayback = mediaType; + + configuration.suppressesIncrementalRendering = [settings cordovaBoolSettingForKey:@"SuppressesIncrementalRendering" defaultValue:NO]; + + /* + * If the old preference key "MediaPlaybackAllowsAirPlay" exists, use it or default to "YES". + * Check if the new preference key "AllowsAirPlayForMediaPlayback" exists and overwrite the "MediaPlaybackAllowsAirPlay" value. + */ + BOOL allowsAirPlayForMediaPlayback = [settings cordovaBoolSettingForKey:@"MediaPlaybackAllowsAirPlay" defaultValue:YES]; + if([settings cordovaSettingForKey:@"AllowsAirPlayForMediaPlayback"] != nil) { + allowsAirPlayForMediaPlayback = [settings cordovaBoolSettingForKey:@"AllowsAirPlayForMediaPlayback" defaultValue:YES]; + } + configuration.allowsAirPlayForMediaPlayback = allowsAirPlayForMediaPlayback; + + /* + * Sets Custom User Agents + * - (Default) "userAgent" is set the the clean user agent. + * E.g. + * UserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" + * + * - If "OverrideUserAgent" is set, it will overwrite the entire "userAgent" value. The "AppendUserAgent" will be iggnored if set. + * Notice: The override logic is handled in the "pluginInitialize" method. + * E.g. + * OverrideUserAgent = "foobar" + * UserAgent = "foobar" + * + * - If "AppendUserAgent" is set and "OverrideUserAgent" is not set, the user defined "AppendUserAgent" will be appended to the "userAgent" + * E.g. + * AppendUserAgent = "foobar" + * UserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 foobar" + */ + NSString *userAgent = configuration.applicationNameForUserAgent; + if ( + [settings cordovaSettingForKey:@"OverrideUserAgent"] == nil && + [settings cordovaSettingForKey:@"AppendUserAgent"] != nil + ) { + userAgent = [NSString stringWithFormat:@"%@ %@", userAgent, [settings cordovaSettingForKey:@"AppendUserAgent"]]; + } + configuration.applicationNameForUserAgent = userAgent; + + if (@available(iOS 13.0, *)) { + NSString *contentMode = [settings cordovaSettingForKey:@"PreferredContentMode"]; + if ([contentMode isEqual: @"mobile"]) { + configuration.defaultWebpagePreferences.preferredContentMode = WKContentModeMobile; + } else if ([contentMode isEqual: @"desktop"]) { + configuration.defaultWebpagePreferences.preferredContentMode = WKContentModeDesktop; + } + + } + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 + if (@available(iOS 14.0, *)) { + configuration.limitsNavigationsToAppBoundDomains = [settings cordovaBoolSettingForKey:@"LimitsNavigationsToAppBoundDomains" defaultValue:NO]; + } +#endif + + return configuration; +} + +- (void)pluginInitialize +{ + // viewController would be available now. we attempt to set all possible delegates to it, by default + CDVViewController* vc = (CDVViewController*)self.viewController; + NSDictionary* settings = self.commandDelegate.settings; + + NSString *scheme = [settings cordovaSettingForKey:@"scheme"]; + + // If scheme is file or nil, then default to file scheme + self.cdvIsFileScheme = [scheme isEqualToString: @"file"] || scheme == nil; + + NSString *hostname = @""; + if(!self.cdvIsFileScheme) { + if(scheme == nil || [WKWebView handlesURLScheme:scheme]){ + scheme = @"app"; + } + vc.appScheme = scheme; + + hostname = [settings cordovaSettingForKey:@"hostname"]; + if(hostname == nil){ + hostname = @"localhost"; + } + + self.CDV_ASSETS_URL = [NSString stringWithFormat:@"%@://%@", scheme, hostname]; + } + + CDVWebViewUIDelegate* uiDelegate = [[CDVWebViewUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]]; + uiDelegate.allowNewWindows = [settings cordovaBoolSettingForKey:@"AllowNewWindows" defaultValue:NO]; + self.uiDelegate = uiDelegate; + + CDVWebViewWeakScriptMessageHandler *weakScriptMessageHandler = [[CDVWebViewWeakScriptMessageHandler alloc] initWithScriptMessageHandler:self]; + + WKUserContentController* userContentController = [[WKUserContentController alloc] init]; + [userContentController addScriptMessageHandler:weakScriptMessageHandler name:CDV_BRIDGE_NAME]; + + if(self.CDV_ASSETS_URL) { + NSString *scriptCode = [NSString stringWithFormat:@"window.CDV_ASSETS_URL = '%@';", self.CDV_ASSETS_URL]; + WKUserScript *wkScript = [[WKUserScript alloc] initWithSource:scriptCode injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; + + if (wkScript) { + [userContentController addUserScript:wkScript]; + } + } + + WKWebViewConfiguration* configuration = [self createConfigurationFromSettings:settings]; + configuration.userContentController = userContentController; + + // Do not configure the scheme handler if the scheme is default (file) + if(!self.cdvIsFileScheme) { + self.schemeHandler = [[CDVURLSchemeHandler alloc] initWithVC:vc]; + [configuration setURLSchemeHandler:self.schemeHandler forURLScheme:scheme]; + } + + // re-create WKWebView, since we need to update configuration + WKWebView* wkWebView = [[WKWebView alloc] initWithFrame:self.engineWebView.frame configuration:configuration]; + wkWebView.UIDelegate = self.uiDelegate; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160400 + // With the introduction of iOS 16.4 the webview is no longer inspectable by default. + // We'll honor that change for release builds, but will still allow inspection on debug builds by default. + // We also introduce an override option, so consumers can influence this decision in their own build. + if (@available(iOS 16.4, *)) { +#ifdef DEBUG + BOOL allowWebviewInspectionDefault = YES; +#else + BOOL allowWebviewInspectionDefault = NO; +#endif + wkWebView.inspectable = [settings cordovaBoolSettingForKey:@"InspectableWebview" defaultValue:allowWebviewInspectionDefault]; + } +#endif + + /* + * This is where the "OverrideUserAgent" is handled. This will replace the entire UserAgent + * with the user defined custom UserAgent. + */ + if ([settings cordovaSettingForKey:@"OverrideUserAgent"] != nil) { + wkWebView.customUserAgent = [settings cordovaSettingForKey:@"OverrideUserAgent"]; + } + + self.engineWebView = wkWebView; + + if ([self.viewController conformsToProtocol:@protocol(WKUIDelegate)]) { + wkWebView.UIDelegate = (id )self.viewController; + } + + if ([self.viewController conformsToProtocol:@protocol(WKNavigationDelegate)]) { + wkWebView.navigationDelegate = (id )self.viewController; + } else { + wkWebView.navigationDelegate = (id )self; + } + + if ([self.viewController conformsToProtocol:@protocol(WKScriptMessageHandler)]) { + [wkWebView.configuration.userContentController addScriptMessageHandler:(id < WKScriptMessageHandler >)self.viewController name:CDV_BRIDGE_NAME]; + } + + [self updateSettings:settings]; + + // check if content thread has died on resume + NSLog(@"%@", @"CDVWebViewEngine will reload WKWebView if required on resume"); + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(onAppWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification object:nil]; + + NSLog(@"Using WKWebView"); + + [self addURLObserver]; +} + +- (void)onReset { + [self addURLObserver]; +} + +static void * KVOContext = &KVOContext; + +- (void)addURLObserver { + if(!IsAtLeastiOSVersion(@"9.0")){ + [self.webView addObserver:self forKeyPath:@"URL" options:0 context:KVOContext]; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == KVOContext) { + if (object == [self webView] && [keyPath isEqualToString: @"URL"] && [object valueForKeyPath:keyPath] == nil){ + NSLog(@"URL is nil. Reloading WKWebView"); + [(WKWebView*)_engineWebView reload]; + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void) onAppWillEnterForeground:(NSNotification*)notification { + if ([self shouldReloadWebView]) { + NSLog(@"%@", @"CDVWebViewEngine reloading!"); + [(WKWebView*)_engineWebView reload]; + } +} + +- (BOOL)shouldReloadWebView +{ + WKWebView* wkWebView = (WKWebView*)_engineWebView; + return [self shouldReloadWebView:wkWebView.URL title:wkWebView.title]; +} + +- (BOOL)shouldReloadWebView:(NSURL*)location title:(NSString*)title +{ + BOOL title_is_nil = (title == nil); + BOOL location_is_blank = [[location absoluteString] isEqualToString:@"about:blank"]; + + BOOL reload = (title_is_nil || location_is_blank); + +#ifdef DEBUG + NSLog(@"%@", @"CDVWebViewEngine shouldReloadWebView::"); + NSLog(@"CDVWebViewEngine shouldReloadWebView title: %@", title); + NSLog(@"CDVWebViewEngine shouldReloadWebView location: %@", [location absoluteString]); + NSLog(@"CDVWebViewEngine shouldReloadWebView reload: %u", reload); +#endif + + return reload; +} + + +- (id)loadRequest:(NSURLRequest*)request +{ + if ([self canLoadRequest:request]) { // can load, differentiate between file urls and other schemes + if(request.URL.fileURL && self.cdvIsFileScheme) { + NSURL* readAccessUrl = [request.URL URLByDeletingLastPathComponent]; + return [(WKWebView*)_engineWebView loadFileURL:request.URL allowingReadAccessToURL:readAccessUrl]; + } else if (request.URL.fileURL) { + NSURL* startURL = [NSURL URLWithString:((CDVViewController *)self.viewController).startPage]; + NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]]; + NSURL *url = [[NSURL URLWithString:self.CDV_ASSETS_URL] URLByAppendingPathComponent:request.URL.path]; + if ([request.URL.path isEqualToString:startFilePath]) { + url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", self.CDV_ASSETS_URL, startURL]]; + } + if(request.URL.query) { + url = [NSURL URLWithString:[@"?" stringByAppendingString:request.URL.query] relativeToURL:url]; + } + if(request.URL.fragment) { + url = [NSURL URLWithString:[@"#" stringByAppendingString:request.URL.fragment] relativeToURL:url]; + } + request = [NSURLRequest requestWithURL:url]; + } + return [(WKWebView*)_engineWebView loadRequest:request]; + } else { // can't load, print out error + NSString* errorHtml = [NSString stringWithFormat: + @"" + @"Error" + @"
" + @"

The WebView engine '%@' is unable to load the request: %@

" + @"

Most likely the cause of the error is that the loading of file urls is not supported in iOS %@.

" + @"
", + NSStringFromClass([self class]), + [request.URL description], + [[UIDevice currentDevice] systemVersion] + ]; + return [self loadHTMLString:errorHtml baseURL:nil]; + } +} + +- (id)loadHTMLString:(NSString*)string baseURL:(NSURL*)baseURL +{ + return [(WKWebView*)_engineWebView loadHTMLString:string baseURL:baseURL]; +} + +- (NSURL*) URL +{ + return [(WKWebView*)_engineWebView URL]; +} + +- (BOOL) canLoadRequest:(NSURLRequest*)request +{ + // See: https://issues.apache.org/jira/browse/CB-9636 + SEL wk_sel = NSSelectorFromString(CDV_WKWEBVIEW_FILE_URL_LOAD_SELECTOR); + + // if it's a file URL, check whether WKWebView has the selector (which is in iOS 9 and up only) + if (request.URL.fileURL) { + return [_engineWebView respondsToSelector:wk_sel]; + } else { + return YES; + } +} + +- (void)updateSettings:(NSDictionary*)settings +{ + WKWebView* wkWebView = (WKWebView*)_engineWebView; + + wkWebView.configuration.preferences.minimumFontSize = [settings cordovaFloatSettingForKey:@"MinimumFontSize" defaultValue:0.0]; + + /* + wkWebView.configuration.preferences.javaScriptEnabled = [settings cordovaBoolSettingForKey:@"JavaScriptEnabled" default:YES]; + wkWebView.configuration.preferences.javaScriptCanOpenWindowsAutomatically = [settings cordovaBoolSettingForKey:@"JavaScriptCanOpenWindowsAutomatically" default:NO]; + */ + + // By default, DisallowOverscroll is false (thus bounce is allowed) + BOOL bounceAllowed = !([settings cordovaBoolSettingForKey:@"DisallowOverscroll" defaultValue:NO]); + + // prevent webView from bouncing + if (!bounceAllowed) { + if ([wkWebView respondsToSelector:@selector(scrollView)]) { + UIScrollView* scrollView = [wkWebView scrollView]; + scrollView.bounces = NO; + scrollView.alwaysBounceVertical = NO; /* iOS 16 workaround */ + scrollView.alwaysBounceHorizontal = NO; /* iOS 16 workaround */ + } else { + for (id subview in wkWebView.subviews) { + if ([[subview class] isSubclassOfClass:[UIScrollView class]]) { + ((UIScrollView*)subview).bounces = NO; + } + } + } + } + + NSString* decelerationSetting = [settings cordovaSettingForKey:@"WKWebViewDecelerationSpeed"]; + + if (![@"fast" isEqualToString:decelerationSetting]) { + [wkWebView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal]; + } else { + [wkWebView.scrollView setDecelerationRate:UIScrollViewDecelerationRateFast]; + } + + wkWebView.allowsBackForwardNavigationGestures = [settings cordovaBoolSettingForKey:@"AllowBackForwardNavigationGestures" defaultValue:NO]; + wkWebView.allowsLinkPreview = [settings cordovaBoolSettingForKey:@"Allow3DTouchLinkPreview" defaultValue:YES]; +} + +- (void)updateWithInfo:(NSDictionary*)info +{ + NSDictionary* scriptMessageHandlers = [info objectForKey:kCDVWebViewEngineScriptMessageHandlers]; + NSDictionary* settings = [info objectForKey:kCDVWebViewEngineWebViewPreferences]; + id navigationDelegate = [info objectForKey:kCDVWebViewEngineWKNavigationDelegate]; + id uiDelegate = [info objectForKey:kCDVWebViewEngineWKUIDelegate]; + + WKWebView* wkWebView = (WKWebView*)_engineWebView; + + if (scriptMessageHandlers && [scriptMessageHandlers isKindOfClass:[NSDictionary class]]) { + NSArray* allKeys = [scriptMessageHandlers allKeys]; + + for (NSString* key in allKeys) { + id object = [scriptMessageHandlers objectForKey:key]; + if ([object conformsToProtocol:@protocol(WKScriptMessageHandler)]) { + [wkWebView.configuration.userContentController addScriptMessageHandler:object name:key]; + } + } + } + + if (navigationDelegate && [navigationDelegate conformsToProtocol:@protocol(WKNavigationDelegate)]) { + wkWebView.navigationDelegate = navigationDelegate; + } + + if (uiDelegate && [uiDelegate conformsToProtocol:@protocol(WKUIDelegate)]) { + wkWebView.UIDelegate = uiDelegate; + } + + if (settings && [settings isKindOfClass:[NSDictionary class]]) { + [self updateSettings:settings]; + } +} + +// This forwards the methods that are in the header that are not implemented here. +// Both WKWebView implement the below: +// loadHTMLString:baseURL: +// loadRequest: +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + return _engineWebView; +} + +- (UIView*)webView +{ + return self.engineWebView; +} + +#pragma mark WKScriptMessageHandler implementation + +- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message +{ + if (![message.name isEqualToString:CDV_BRIDGE_NAME]) { + return; + } + + CDVViewController* vc = (CDVViewController*)self.viewController; + + NSArray* jsonEntry = message.body; // NSString:callbackId, NSString:service, NSString:action, NSArray:args + CDVInvokedUrlCommand* command = [CDVInvokedUrlCommand commandFromJson:jsonEntry]; + CDV_EXEC_LOG(@"Exec(%@): Calling %@.%@", command.callbackId, command.className, command.methodName); + + if (![vc.commandQueue execute:command]) { +#ifdef DEBUG + NSError* error = nil; + NSString* commandJson = nil; + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:jsonEntry + options:0 + error:&error]; + + if (error == nil) { + commandJson = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + } + + static NSUInteger maxLogLength = 1024; + NSString* commandString = ([commandJson length] > maxLogLength) ? + [NSString stringWithFormat : @"%@[...]", [commandJson substringToIndex:maxLogLength]] : + commandJson; + + NSLog(@"FAILED pluginJSON = %@", commandString); +#endif + } +} + +#pragma mark WKNavigationDelegate implementation + +- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation +{ + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginResetNotification object:webView]]; +} + +- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation +{ + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPageDidLoadNotification object:webView]]; +} + +- (void)webView:(WKWebView*)theWebView didFailProvisionalNavigation:(WKNavigation*)navigation withError:(NSError*)error +{ + [self webView:theWebView didFailNavigation:navigation withError:error]; +} + +- (void)webView:(WKWebView*)theWebView didFailNavigation:(WKNavigation*)navigation withError:(NSError*)error +{ + CDVViewController* vc = (CDVViewController*)self.viewController; + + NSString* message = [NSString stringWithFormat:@"Failed to load webpage with error: %@", [error localizedDescription]]; + NSLog(@"%@", message); + + NSURL* errorUrl = vc.errorURL; + if (errorUrl) { + NSCharacterSet *charSet = [NSCharacterSet URLFragmentAllowedCharacterSet]; + errorUrl = [NSURL URLWithString:[NSString stringWithFormat:@"?error=%@", [message stringByAddingPercentEncodingWithAllowedCharacters:charSet]] relativeToURL:errorUrl]; + NSLog(@"%@", [errorUrl absoluteString]); + [theWebView loadRequest:[NSURLRequest requestWithURL:errorUrl]]; + } +} + +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView +{ + [webView reload]; +} + +- (BOOL)defaultResourcePolicyForURL:(NSURL*)url +{ + // all file:// urls are allowed + if ([url isFileURL]) { + return YES; + } + + return NO; +} + +- (void) webView: (WKWebView *) webView decidePolicyForNavigationAction: (WKNavigationAction*) navigationAction decisionHandler: (void (^)(WKNavigationActionPolicy)) decisionHandler +{ + NSURL* url = [navigationAction.request URL]; + CDVViewController* vc = (CDVViewController*)self.viewController; + + /* + * Give plugins the chance to handle the url + */ + BOOL anyPluginsResponded = NO; + BOOL shouldAllowRequest = NO; + + for (NSString* pluginName in vc.pluginObjects) { + CDVPlugin* plugin = [vc.pluginObjects objectForKey:pluginName]; + SEL selector = NSSelectorFromString(@"shouldOverrideLoadWithRequest:navigationType:"); + if ([plugin respondsToSelector:selector]) { + anyPluginsResponded = YES; + // https://issues.apache.org/jira/browse/CB-12497 + int navType = (int)navigationAction.navigationType; + shouldAllowRequest = (((BOOL (*)(id, SEL, id, int))objc_msgSend)(plugin, selector, navigationAction.request, navType)); + if (!shouldAllowRequest) { + break; + } + } + } + + if (anyPluginsResponded) { + return decisionHandler(shouldAllowRequest); + } + + /* + * Handle all other types of urls (tel:, sms:), and requests to load a url in the main webview. + */ + BOOL shouldAllowNavigation = [self defaultResourcePolicyForURL:url]; + if (shouldAllowNavigation) { + return decisionHandler(YES); + } else { + [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:CDVPluginHandleOpenURLNotification object:url]]; + } + + return decisionHandler(NO); +} + +#pragma mark - Plugin interface + +- (void)allowsBackForwardNavigationGestures:(CDVInvokedUrlCommand*)command; +{ + id value = [command argumentAtIndex:0]; + if (!([value isKindOfClass:[NSNumber class]])) { + value = [NSNumber numberWithBool:NO]; + } + + WKWebView* wkWebView = (WKWebView*)_engineWebView; + wkWebView.allowsBackForwardNavigationGestures = [value boolValue]; +} + +@end + +#pragma mark - CDVWebViewWeakScriptMessageHandler + +@implementation CDVWebViewWeakScriptMessageHandler + +- (instancetype)initWithScriptMessageHandler:(id)scriptMessageHandler +{ + self = [super init]; + if (self) { + _scriptMessageHandler = scriptMessageHandler; + } + return self; +} + +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message +{ + [self.scriptMessageHandler userContentController:userContentController didReceiveScriptMessage:message]; +} + +@end diff --git a/testapp-cordova/src/App.tsx b/testapp-cordova/src/App.tsx index 4edebe2..4b1d7d1 100644 --- a/testapp-cordova/src/App.tsx +++ b/testapp-cordova/src/App.tsx @@ -14,99 +14,9 @@ // limitations under the License. // -import { getInteractiveLibraryTests, getLibraryTests, getTestbedTests } from '../_tests/AllTests' -import { getTestConfig } from './Config' -import { TestContext, UserPromptDuration, UserInteraction, TestCounter, TestProgressObserver } from './testbed' -import { TestLog } from './testbed/TestLog' -import { TestMonitorGroup } from './testbed/TestMonitor' -import { TestRunner } from './testbed/TestRunner' - -// TODO: this is copied from the react native app. improve - extract instead of copy -class TestExecutor implements UserInteraction { +// @ts-nocheck - private isRunning = false - private readonly onShowPrompt: (context: TestContext, message: string, duration: number) => Promise - private readonly onProgress: TestProgressObserver - private readonly onCompletion: (inProgress: boolean) => void - private testRunner?: TestRunner - - - constructor( - onShowPrompt: (context: TestContext, message: string, duration: number) => Promise, - onProgress: TestProgressObserver, - onCompletion: (inProgress: boolean)=>void) { - this.onShowPrompt = onShowPrompt - this.onProgress = onProgress - this.onCompletion = onCompletion - this.runTests(false) - } - - async runTests(interactive: boolean) { - if (this.isRunning) { - console.warn('Tests are still in progress...'); - return - } - this.onCompletion(true) - this.isRunning = true - - const cfg = await getTestConfig() - const logger = new TestLog() - const monitor = new TestMonitorGroup([ logger ]) - const runner = this.testRunner = new TestRunner('Automatic tests', cfg, monitor, this) - runner.allTestsCounter.addObserver(this.onProgress) - const tests = interactive ? getInteractiveLibraryTests() : getLibraryTests().concat(getTestbedTests()) - try { - await runner.runTests(tests) - } catch (e) { - console.log("Run Tests failed"); - console.error(e); - } - this.isRunning = false - this.testRunner = undefined - this.onCompletion(false) - } - - cancelTests() { - this.testRunner?.cancelRunningTests() - } - - stillRunnint(): boolean { - return this.isRunning - } - - async showPrompt(context: TestContext, message: string, duration: UserPromptDuration): Promise { - let sleepDuration: number - if (duration === UserPromptDuration.QUICK) { - sleepDuration = 500 - } else if (duration === UserPromptDuration.SHORT) { - sleepDuration = 2000 - } else { - sleepDuration = 5000 - } - return await this.onShowPrompt(context, message, sleepDuration) - } - - async sleepWithProgress(context: TestContext, durationMs: number): Promise { - let remaining = durationMs - while (remaining > 0) { - if (remaining >= 1000) { - const timeInSeconds = Math.round(remaining * 0.001) - if (timeInSeconds > 1) { - await this.onShowPrompt(context, `Sleeping for ${timeInSeconds} seconds...`, 1000) - } else { - await this.onShowPrompt(context, `Finishing sleep...`, 1000) - } - remaining -= 1000 - } else { - // Otherwise just sleep for the remaining time - await new Promise(resolve => setTimeout(resolve, remaining)) - remaining = 0 - } - } - } -} - -declare var cordova: any; +import { TestExecutor } from './TestExecutor' document.addEventListener('deviceready', onDeviceReady, false); @@ -119,16 +29,13 @@ function onDeviceReady() { console.log('Running cordova-' + cordova.platformId + '@' + cordova.version); document.getElementById('deviceready').classList.add('ready'); - document.getElementById('deviceready').addEventListener('click', async function (e) { - - }) const executor = new TestExecutor(async (_context, message, duration) => { console.log(message) await new Promise(resolve => setTimeout(resolve, duration)) }, (progress) => { - progressEl.innerHTML = `${progress.succeeded} succeeded
${progress.failed} failed
${progress.skipped} skipped
out of total ${progress.total}`; - }, (progress) => { - statusEl.innerHTML = progress ? "Tests running" : "Tests finished"; + progressEl.innerHTML = `${progress.succeeded} succeeded
${progress.failed} failed
${progress.skipped} skipped
out of total ${progress.total}`;; + }, (finished) => { + statusEl.innerHTML = finished ? "Tests running" : "Tests finished"; }) } \ No newline at end of file diff --git a/testapp-cordova/www/index.html b/testapp-cordova/www/index.html index 11611f4..cad3fb2 100644 --- a/testapp-cordova/www/index.html +++ b/testapp-cordova/www/index.html @@ -24,7 +24,7 @@ default-src * data: blob: ws: wss: gap://ready file://*; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'; - connect-src * ws: wss:;"> + connect-src * localhost: ws: wss:;"> diff --git a/testapp/package-lock.json b/testapp/package-lock.json index ac47967..e957980 100644 --- a/testapp/package-lock.json +++ b/testapp/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "dependencies": { "chalk": "^4.1.0", - "powerauth-js-test-client": "^0.2.9", + "powerauth-js-test-client": "^0.2.12", "react": "18.2.0", "react-native": "0.71.6", "react-native-config": "^1.5.0", @@ -8668,9 +8668,10 @@ } }, "node_modules/powerauth-js-test-client": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/powerauth-js-test-client/-/powerauth-js-test-client-0.2.9.tgz", - "integrity": "sha512-tam5DpQajZ/442WoXCU3tVNLSkJVBINrvsrvkhQ04qA5wRx/GrcMT4JT48Up7iSzvcfMv9ky2DmYdmIKZPD3jA==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/powerauth-js-test-client/-/powerauth-js-test-client-0.2.12.tgz", + "integrity": "sha512-PhYSVQU+oMPiwhDD5H8OGXMuU0eQ0RKqfAKlL7aCUNXvp3dKNF5ib1Y7jtrUxI1telpBVXONOX8IGLjLNLDNGA==", + "license": "Apache 2.0", "dependencies": { "cross-fetch": "^3.1.5", "js-base64": "^3.7.2" @@ -8963,7 +8964,7 @@ "node_modules/react-native-powerauth-mobile-sdk": { "version": "2.5.0", "resolved": "file:../build/react-native/react-native-powerauth-mobile-sdk-2.5.0.tgz", - "integrity": "sha512-Mb7YZ9y+WtExXV2F6dpCQ16/xQhpCdui1Lcs/zIfcjlDutWIRMqTKtHY/gwwH0AlPFoQiFdtg8IHBvwPheSQlw==", + "integrity": "sha512-vvFbXx8TUN3CaqgLt4J7jPsQqSDv3WrrCGOXPi/KGxi9b8AFyDLHYR1xpbMDvxBGhVu+sdczYm4twdszPp8U0g==", "license": "Apache 2.0", "dependencies": { "node-fetch": ">=2.6.1" @@ -17191,9 +17192,9 @@ "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==" }, "powerauth-js-test-client": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/powerauth-js-test-client/-/powerauth-js-test-client-0.2.9.tgz", - "integrity": "sha512-tam5DpQajZ/442WoXCU3tVNLSkJVBINrvsrvkhQ04qA5wRx/GrcMT4JT48Up7iSzvcfMv9ky2DmYdmIKZPD3jA==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/powerauth-js-test-client/-/powerauth-js-test-client-0.2.12.tgz", + "integrity": "sha512-PhYSVQU+oMPiwhDD5H8OGXMuU0eQ0RKqfAKlL7aCUNXvp3dKNF5ib1Y7jtrUxI1telpBVXONOX8IGLjLNLDNGA==", "requires": { "cross-fetch": "^3.1.5", "js-base64": "^3.7.2" @@ -17412,7 +17413,7 @@ }, "react-native-powerauth-mobile-sdk": { "version": "file:../build/react-native/react-native-powerauth-mobile-sdk-2.5.0.tgz", - "integrity": "sha512-Mb7YZ9y+WtExXV2F6dpCQ16/xQhpCdui1Lcs/zIfcjlDutWIRMqTKtHY/gwwH0AlPFoQiFdtg8IHBvwPheSQlw==", + "integrity": "sha512-vvFbXx8TUN3CaqgLt4J7jPsQqSDv3WrrCGOXPi/KGxi9b8AFyDLHYR1xpbMDvxBGhVu+sdczYm4twdszPp8U0g==", "requires": { "node-fetch": ">=2.6.1" } diff --git a/testapp/package.json b/testapp/package.json index f144a76..2c340f3 100644 --- a/testapp/package.json +++ b/testapp/package.json @@ -3,17 +3,17 @@ "version": "0.0.1", "private": true, "scripts": { - "android": "react-native run-android", - "ios": "react-native run-ios", - "start": "react-native start", + "android": "npx react-native run-android", + "ios": "npx react-native run-ios", + "start": "npx react-native start", "pods": "pushd ios; pod install; popd", "test": "echo Just run this app; exit 1", "lint": "eslint .", - "reinstallPlugin": "npm r react-native-powerauth-mobile-sdk && npm i ../build/react-native/react-native-powerauth-mobile-sdk-2.5.0.tgz" + "reinstallPlugin": "(npm r react-native-powerauth-mobile-sdk || true) && npm i ../build/react-native/react-native-powerauth-mobile-sdk-2.5.0.tgz" }, "dependencies": { "chalk": "^4.1.0", - "powerauth-js-test-client": "^0.2.9", + "powerauth-js-test-client": "^0.2.12", "react": "18.2.0", "react-native": "0.71.6", "react-native-config": "^1.5.0", diff --git a/testapp/src/App.tsx b/testapp/src/App.tsx index 5c5f6b4..b9c8538 100644 --- a/testapp/src/App.tsx +++ b/testapp/src/App.tsx @@ -24,92 +24,7 @@ import { Appearance, NativeEventSubscription, } from 'react-native' -import { getInteractiveLibraryTests, getLibraryTests, getTestbedTests } from '../_tests/AllTests' -import { getTestConfig } from './Config' -import { TestContext, UserPromptDuration, UserInteraction, TestCounter, TestProgressObserver } from './testbed' -import { TestLog } from './testbed/TestLog' -import { TestMonitorGroup } from './testbed/TestMonitor' -import { TestRunner } from './testbed/TestRunner' - - -class TestExecutor implements UserInteraction { - - private isRunning = false - private readonly onShowPrompt: (context: TestContext, message: string, duration: number) => Promise - private readonly onProgress: TestProgressObserver - private readonly onCompletion: (inProgress: boolean) => void - private testRunner?: TestRunner - - - constructor( - onShowPrompt: (context: TestContext, message: string, duration: number) => Promise, - onProgress: TestProgressObserver, - onCompletion: (inProgress: boolean)=>void) { - this.onShowPrompt = onShowPrompt - this.onProgress = onProgress - this.onCompletion = onCompletion - this.runTests(false) - } - - async runTests(interactive: boolean) { - if (this.isRunning) { - console.warn('Tests are still in progress...'); - return - } - this.onCompletion(true) - this.isRunning = true - - const cfg = await getTestConfig() - const logger = new TestLog() - const monitor = new TestMonitorGroup([ logger ]) - const runner = this.testRunner = new TestRunner('Automatic tests', cfg, monitor, this) - runner.allTestsCounter.addObserver(this.onProgress) - const tests = interactive ? getInteractiveLibraryTests() : getLibraryTests().concat(getTestbedTests()) - await runner.runTests(tests) - this.isRunning = false - this.testRunner = undefined - this.onCompletion(false) - } - - cancelTests() { - this.testRunner?.cancelRunningTests() - } - - stillRunnint(): boolean { - return this.isRunning - } - - async showPrompt(context: TestContext, message: string, duration: UserPromptDuration): Promise { - let sleepDuration: number - if (duration === UserPromptDuration.QUICK) { - sleepDuration = 500 - } else if (duration === UserPromptDuration.SHORT) { - sleepDuration = 2000 - } else { - sleepDuration = 5000 - } - return await this.onShowPrompt(context, message, sleepDuration) - } - - async sleepWithProgress(context: TestContext, durationMs: number): Promise { - let remaining = durationMs - while (remaining > 0) { - if (remaining >= 1000) { - const timeInSeconds = Math.round(remaining * 0.001) - if (timeInSeconds > 1) { - await this.onShowPrompt(context, `Sleeping for ${timeInSeconds} seconds...`, 1000) - } else { - await this.onShowPrompt(context, `Finishing sleep...`, 1000) - } - remaining -= 1000 - } else { - // Otherwise just sleep for the remaining time - await new Promise(resolve => setTimeout(resolve, remaining)) - remaining = 0 - } - } - } -} +import { TestExecutor } from './TestExecutor' interface AppState { isDark: boolean diff --git a/testapp/src/TestExecutor.ts b/testapp/src/TestExecutor.ts new file mode 100644 index 0000000..0cd6eb1 --- /dev/null +++ b/testapp/src/TestExecutor.ts @@ -0,0 +1,150 @@ +// +// Copyright 2022 Wultra s.r.o. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { getInteractiveLibraryTests, getLibraryTests, getTestbedTests } from '../_tests/AllTests' +import { getTestConfig } from './Config' +import { TestContext, UserPromptDuration, UserInteraction, TestProgressObserver, TestProgress } from './testbed' +import { TestLog } from './testbed/TestLog' +import { TestMonitorGroup } from './testbed/TestMonitor' +import { TestRunner } from './testbed/TestRunner' + +export class TestServer { + + constructor() { + // we want to re-route console outputs for easier "test infrastructure" and debugging on CI + const logF = console.log; + const warnF = console.warn; + const errorF = console.error; + const infoF = console.info; + + console.log = (...params) => { + this.log(params) + logF(...params); + } + + console.warn = (...params) => { + this.log(params) + warnF(...params); + } + + console.info = (...params) => { + this.log(params) + infoF(...params); + } + + console.error = (...params) => { + this.log(params) + errorF(...params); + } + } + + log(data: any[]) { + this.call("log", data) + } + + reportStatus(data: TestProgress) { + this.call("reportStatus", data) + } + + private call(method: string, object: any) { + // the server code is in the git root as "test-listener.js" + fetch("http://localhost:8083/" + method, { method: "POST", body: JSON.stringify(object) }).catch((e) => { + // do we need to react? + }) + } +} + +export class TestExecutor implements UserInteraction { + + private isRunning = false + private readonly onShowPrompt: (context: TestContext, message: string, duration: number) => Promise + private readonly onProgress: TestProgressObserver + private readonly onCompletion: (inProgress: boolean) => void + private testRunner?: TestRunner + private testServer = new TestServer(); + + constructor( + onShowPrompt: (context: TestContext, message: string, duration: number) => Promise, + onProgress: TestProgressObserver, + onCompletion: (inProgress: boolean)=>void) { + this.onShowPrompt = onShowPrompt + this.onProgress = (progress) => { + onProgress(progress) + this.testServer.reportStatus(progress) + } + this.onCompletion = onCompletion + this.runTests(false) + } + + async runTests(interactive: boolean) { + if (this.isRunning) { + console.warn('Tests are still in progress...'); + return + } + this.onCompletion(true) + this.isRunning = true + + const cfg = await getTestConfig() + const logger = new TestLog() + const monitor = new TestMonitorGroup([ logger ]) + const runner = this.testRunner = new TestRunner('Automatic tests', cfg, monitor, this) + runner.allTestsCounter.addObserver(this.onProgress) + const tests = interactive ? getInteractiveLibraryTests() : getLibraryTests().concat(getTestbedTests()) + await runner.runTests(tests) + this.isRunning = false + this.testRunner = undefined + this.onCompletion(false) + } + + cancelTests() { + this.testRunner?.cancelRunningTests() + } + + stillRunnint(): boolean { + return this.isRunning + } + + async showPrompt(context: TestContext, message: string, duration: UserPromptDuration): Promise { + let sleepDuration: number + if (duration === UserPromptDuration.QUICK) { + sleepDuration = 500 + } else if (duration === UserPromptDuration.SHORT) { + sleepDuration = 2000 + } else { + sleepDuration = 5000 + } + return await this.onShowPrompt(context, message, sleepDuration) + } + + async sleepWithProgress(context: TestContext, durationMs: number): Promise { + let remaining = durationMs + while (remaining > 0) { + if (remaining >= 1000) { + const timeInSeconds = Math.round(remaining * 0.001) + if (timeInSeconds > 1) { + await this.onShowPrompt(context, `Sleeping for ${timeInSeconds} seconds...`, 1000) + } else { + await this.onShowPrompt(context, `Finishing sleep...`, 1000) + } + remaining -= 1000 + } else { + // Otherwise just sleep for the remaining time + await new Promise(resolve => setTimeout(resolve, remaining)) + remaining = 0 + } + } + } +} \ No newline at end of file