From 949ed1feb6d9546f94696949782be2efbf70c222 Mon Sep 17 00:00:00 2001 From: Andre Destro Date: Fri, 5 Sep 2025 09:57:14 +0100 Subject: [PATCH 1/2] ci: update the workflows --- CODEOWNERS => .github/CODEOWNERS | 0 .github/actions/build-xcframework/action.yml | 52 ++++ .../actions/get-project-version/action.yml | 45 +++ .../actions/install-dependencies/action.yml | 50 ++++ .../actions/package-xcframework/action.yaml | 34 +++ .github/actions/set-xcode-version/action.yml | 18 ++ .../pull_request_template.md | 4 +- .github/workflows/ci.yml | 180 +++++++++++ .github/workflows/continuous_integration.yml | 51 ---- .github/workflows/prepare_release.yml | 176 +++++++---- .github/workflows/release.yml | 145 +++++++++ .github/workflows/release_and_publish.yml | 67 ----- Gemfile | 4 - Gemfile.lock | 279 ------------------ fastlane/Appfile | 6 - fastlane/Fastfile | 45 --- 16 files changed, 648 insertions(+), 508 deletions(-) rename CODEOWNERS => .github/CODEOWNERS (100%) create mode 100644 .github/actions/build-xcframework/action.yml create mode 100644 .github/actions/get-project-version/action.yml create mode 100644 .github/actions/install-dependencies/action.yml create mode 100644 .github/actions/package-xcframework/action.yaml create mode 100644 .github/actions/set-xcode-version/action.yml rename pull_request_template.md => .github/pull_request_template.md (92%) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/continuous_integration.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/release_and_publish.yml delete mode 100644 Gemfile delete mode 100644 Gemfile.lock delete mode 100644 fastlane/Appfile delete mode 100644 fastlane/Fastfile diff --git a/CODEOWNERS b/.github/CODEOWNERS similarity index 100% rename from CODEOWNERS rename to .github/CODEOWNERS diff --git a/.github/actions/build-xcframework/action.yml b/.github/actions/build-xcframework/action.yml new file mode 100644 index 0000000..55afdf8 --- /dev/null +++ b/.github/actions/build-xcframework/action.yml @@ -0,0 +1,52 @@ +name: Build XCFramework +description: Build XCFramework for iOS and iOS Simulator + +inputs: + project_name: + description: Name of the Xcode project or scheme + required: true + +outputs: + xcframework_path: + description: Final path to the generated XCFramework + value: ${{ steps.build.outputs.xcframework_path }} + +runs: + using: "composite" + steps: + - name: Build XCFramework + id: build + shell: bash + run: | + set -euo pipefail + + PROJECT_NAME="${{ inputs.project_name }}" + BUILD_DIR="./build" + + echo "šŸ› ļø Building XCFramework..." + + xcodebuild archive \ + -scheme "$PROJECT_NAME" \ + -configuration Release \ + -destination 'generic/platform=iOS Simulator' \ + -archivePath "$BUILD_DIR/${PROJECT_NAME}.framework-iphonesimulator.xcarchive" \ + SKIP_INSTALL=NO \ + BUILD_LIBRARIES_FOR_DISTRIBUTION=YES | xcbeautify + + xcodebuild archive \ + -scheme "$PROJECT_NAME" \ + -configuration Release \ + -destination 'generic/platform=iOS' \ + -archivePath "$BUILD_DIR/${PROJECT_NAME}.framework-iphoneos.xcarchive" \ + SKIP_INSTALL=NO \ + BUILD_LIBRARIES_FOR_DISTRIBUTION=YES | xcbeautify + + XCFRAMEWORK_PATH="$BUILD_DIR/${PROJECT_NAME}.xcframework" + + xcodebuild -create-xcframework \ + -framework "$BUILD_DIR/${PROJECT_NAME}.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/${PROJECT_NAME}.framework" \ + -framework "$BUILD_DIR/${PROJECT_NAME}.framework-iphoneos.xcarchive/Products/Library/Frameworks/${PROJECT_NAME}.framework" \ + -output "$XCFRAMEWORK_PATH" + + echo "āœ… XCFramework built successfully" + echo "xcframework_path=$XCFRAMEWORK_PATH" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/get-project-version/action.yml b/.github/actions/get-project-version/action.yml new file mode 100644 index 0000000..5ee6a08 --- /dev/null +++ b/.github/actions/get-project-version/action.yml @@ -0,0 +1,45 @@ +name: Get Project Version +description: Extracts MARKETING_VERSION from a .pbxproj file + +inputs: + project_name: + description: Name of the Xcode project (without .xcodeproj) + required: false + pbxproj_path: + description: Path to the project.pbxproj file (overrides project_name if provided) + required: false + +outputs: + version: + description: The extracted project version + value: ${{ steps.extract.outputs.version }} + +runs: + using: "composite" + steps: + - name: Extract current project version + id: extract + shell: bash + run: | + set -euo pipefail + + # Determine the pbxproj path + if [[ -n "${{ inputs.pbxproj_path }}" ]]; then + PBXPROJ="${{ inputs.pbxproj_path }}" + elif [[ -n "${{ inputs.project_name }}" ]]; then + PBXPROJ="${{ inputs.project_name }}.xcodeproj/project.pbxproj" + else + echo "āŒ Either 'project_name' or 'pbxproj_path' must be provided." + exit 1 + fi + + # Check if the pbxproj file exists + if [[ ! -f "$PBXPROJ" ]]; then + echo "āŒ Project file not found: $PBXPROJ" + exit 1 + fi + + echo "šŸ“¦ Extracting current MARKETING_VERSION from $PBXPROJ..." + VERSION=$(grep -m1 'MARKETING_VERSION =' "$PBXPROJ" | sed -E 's/.*MARKETING_VERSION = ([^;]+);/\1/' | xargs) + echo "šŸ”¢ Current version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml new file mode 100644 index 0000000..b300ab2 --- /dev/null +++ b/.github/actions/install-dependencies/action.yml @@ -0,0 +1,50 @@ +name: Install Dependencies +description: Checks for Homebrew and installs any missing CLI tools and Ruby gems + +inputs: + tools: + description: Space-separated list of tools to check and install via Homebrew + required: false + gems: + description: Space-separated list of gems to check and install via gem + required: false + +runs: + using: "composite" + steps: + - name: Install Dependencies + shell: bash + run: | + set -euo pipefail + + if [ -n "${{ inputs.tools }}" ]; then + echo "šŸ” Checking for Homebrew..." + if ! command -v brew >/dev/null; then + echo "āŒ Homebrew is required but not installed. Aborting." + exit 1 + fi + + echo "šŸ”§ Installing missing brew tools..." + for tool in ${{ inputs.tools }}; do + if command -v "$tool" >/dev/null; then + echo "āœ… $tool is already installed." + else + echo "šŸ“¦ Installing $tool via brew..." + brew install "$tool" + fi + done + fi + + if [ -n "${{ inputs.gems }}" ]; then + echo "šŸ”§ Installing missing gems..." + for gem in ${{ inputs.gems }}; do + if gem list -i "$gem" >/dev/null; then + echo "āœ… $gem gem is already installed." + else + echo "šŸ’Ž Installing $gem via gem..." + gem install "$gem" + fi + done + fi + + echo "āœ… All dependencies are ready." diff --git a/.github/actions/package-xcframework/action.yaml b/.github/actions/package-xcframework/action.yaml new file mode 100644 index 0000000..cb6154a --- /dev/null +++ b/.github/actions/package-xcframework/action.yaml @@ -0,0 +1,34 @@ +name: Package XCFramework and LICENSE +description: Zips the built XCFramework and LICENSE into a versioned release artifact + +inputs: + package_name: + description: The name of the package (e.g., MyLibrary-1.0.0) + required: true + xcframework_path: + description: The path to the built .xcframework + required: true + license_path: + description: The path to the LICENSE file + required: true + +outputs: + zip_name: + description: The name of the created zip file + value: ${{ steps.package.outputs.zip_name }} + +runs: + using: "composite" + steps: + - id: package + shell: bash + run: | + set -euo pipefail + ZIP_NAME="${{ inputs.package_name }}.zip" + mkdir -p release + cp -R "${{ inputs.xcframework_path }}" release/ + cp "${{ inputs.license_path }}" release/ + cd release + zip -r "../$ZIP_NAME" . + echo "āœ… Packaged XCFramework and LICENSE into $ZIP_NAME" + echo "zip_name=$ZIP_NAME" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/set-xcode-version/action.yml b/.github/actions/set-xcode-version/action.yml new file mode 100644 index 0000000..8432bca --- /dev/null +++ b/.github/actions/set-xcode-version/action.yml @@ -0,0 +1,18 @@ +name: Set Xcode Version +description: Selects the desired Xcode version using xcode-select. +inputs: + xcode-version: + description: The Xcode version to select (e.g., 16.4) + required: true +runs: + using: 'composite' + steps: + - run: | + set -e + echo "Setting Xcode version to ${{ inputs.xcode-version }}..." + if ! sudo xcode-select -s /Applications/Xcode_${{ inputs.xcode-version }}.app/Contents/Developer; then + echo "āŒ Failed to select Xcode ${{ inputs.xcode-version }}. Listing available Xcodes:" + ls /Applications | grep Xcode + exit 1 + fi + shell: bash diff --git a/pull_request_template.md b/.github/pull_request_template.md similarity index 92% rename from pull_request_template.md rename to .github/pull_request_template.md index cf46729..826cef6 100644 --- a/pull_request_template.md +++ b/.github/pull_request_template.md @@ -10,7 +10,7 @@ - [ ] Fix (non-breaking change which fixes an issue) - [ ] Feature (non-breaking change which adds functionality) - [ ] Refactor (cosmetic changes) -- [ ] Breaking change (change that would cause existing functionality not to work as expected) +- [ ] Breaking change (change that would cause existing functionality to not work as expected) ## Tests @@ -21,7 +21,7 @@ ## Checklist - [ ] Pull request title follows the format `RNMT-XXXX ` -- [ ] Code follows the code style of this project +- [ ] Code follows code style of this project - [ ] CHANGELOG.md file is correctly updated - [ ] Changes require an update to the documentation - [ ] Documentation has been updated accordingly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c704144 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,180 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write # For PR comments + +env: + PROJECT_NAME: OSInAppBrowserLib + SCHEME_NAME: OSInAppBrowserLib + XCODEPROJ_PATH: OSInAppBrowserLib.xcodeproj + XCODE_VERSION: 16.4 + DESTINATION: 'platform=iOS Simulator,OS=latest,name=iPhone 16' + COVERAGE_TARGET_FILTER: OSInAppBrowserLib + BUILD_REPORTS_DIR: build/reports + SONAR_REPORTS_DIR: sonar-reports + +jobs: + test: + name: Run Tests + runs-on: macos-15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + with: + tools: swiftlint xcbeautify + + - name: Set Xcode version + uses: ./.github/actions/set-xcode-version + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Run Unit Tests + id: unit_tests + env: + SCHEME_NAME: ${{ env.SCHEME_NAME }} + XCODEPROJ_PATH: ${{ env.XCODEPROJ_PATH }} + IOS_SIMULATOR_DEVICE: ${{ env.IOS_SIMULATOR_DEVICE }} + DESTINATION: ${{ env.DESTINATION }} + run: | + set -euo pipefail + XCRESULT_NAME="TestResults.xcresult" + mkdir -p "$BUILD_REPORTS_DIR" + xcodebuild test \ + -project "$XCODEPROJ_PATH" \ + -scheme "$SCHEME_NAME" \ + -destination "$DESTINATION" \ + -configuration Debug \ + -enableCodeCoverage YES \ + -resultBundlePath "$XCRESULT_NAME" \ + SKIP_SCRIPT_PHASES=YES \ + CODE_SIGNING_ALLOWED=NO | xcbeautify --report junit --report-path "$BUILD_REPORTS_DIR" + echo "xcresult_name=$XCRESULT_NAME" >> "$GITHUB_OUTPUT" + + - name: Generate Code Coverage Report for SonarQube + continue-on-error: true + env: + XCRESULT_NAME: ${{ steps.unit_tests.outputs.xcresult_name }} + run: | + set -euo pipefail + echo "šŸ” Generating SonarQube coverage report..." + + if [ ! -d "$XCRESULT_NAME" ]; then + echo "āš ļø $XCRESULT_NAME not found. Skipping coverage report generation." + exit 0 + fi + + mkdir -p ${{ env.SONAR_REPORTS_DIR }} + + echo "šŸ“¦ Downloading coverage converter script..." + curl -sSL https://raw.githubusercontent.com/SonarSource/sonar-scanning-examples/master/swift-coverage/swift-coverage-example/xccov-to-sonarqube-generic.sh -o xccov-to-sonarqube-generic.sh + chmod +x xccov-to-sonarqube-generic.sh + + echo "šŸ“ Running coverage converter..." + ./xccov-to-sonarqube-generic.sh TestResults.xcresult > ${{ env.SONAR_REPORTS_DIR }}/sonarqube-generic-coverage.xml + echo "āœ… SonarQube coverage report generated successfully" + + - name: Run SwiftLint for SonarQube + run: | + set -euo pipefail + echo "šŸ” Running SwiftLint..." + mkdir -p ${{ env.SONAR_REPORTS_DIR }} + swiftlint --reporter checkstyle > "${{ env.SONAR_REPORTS_DIR }}/swiftlint.xml" || { + echo "āš ļø SwiftLint finished with issues." + exit 0 + } + echo "āœ… SwiftLint report generated successfully" + + - name: Setup SonarQube Scanner + uses: warchant/setup-sonar-scanner@v8 + + - name: Send to SonarCloud + id: sonarcloud + continue-on-error: true + run: | + set -euo pipefail + if [ -z "${{ secrets.SONAR_TOKEN }}" ]; then + echo "āš ļø SONAR_TOKEN secret is not set. Skipping SonarCloud analysis." + exit 0 + fi + if [ -f "sonar-project.properties" ]; then + echo "šŸ” Sending results to SonarCloud..." + echo "šŸ“¦ Commit: ${{ github.sha }}" + if [ "${{ github.ref_name }}" = "main" ]; then + echo "🌟 Analyzing main branch" + sonar-scanner + else + echo "🌿 Analyzing feature branch: ${{ github.ref_name }}" + sonar-scanner -Dsonar.branch.name="${{ github.ref_name }}" + fi + else + echo "āš ļø sonar-project.properties not found, skipping SonarCloud" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: | + ${{ steps.unit_tests.outputs.xcresult_name }} + ${{ env.SONAR_REPORTS_DIR }} + ${{ env.BUILD_REPORTS_DIR }} + + - name: Comment Test Results + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + env: + XCRESULT_NAME: ${{ steps.unit_tests.outputs.xcresult_name }} + COVERAGE_TARGET_FILTER: ${{ env.COVERAGE_TARGET_FILTER }} + with: + script: | + const { execSync } = require('child_process'); + const fs = require('fs'); + + console.log('šŸ“ Starting to comment test results...'); + let coveragePercentage = 'N/A'; + try { + const xcresultName = process.env.XCRESULT_NAME; + const coverageTarget = process.env.COVERAGE_TARGET_FILTER; + console.log(`Checking result file: ${xcresultName}`); + if (fs.existsSync(xcresultName)) { + console.log('Result file found. Calculating coverage...'); + const output = execSync(`xcrun xccov view --report "${xcresultName}"`).toString(); + const match = output.match(new RegExp(`${coverageTarget}.*?([0-9]+\\.[0-9]+%)`)); + if (match && match[1]) { + coveragePercentage = match[1]; + console.log(`Coverage found: ${coveragePercentage}`); + } else { + console.log('Coverage not found in report.'); + } + } else { + console.log('Result file not found.'); + } + } catch (e) { + console.error('Error calculating coverage:', e); + coveragePercentage = 'N/A'; + } + + console.log('Commenting on PR with test results and coverage...'); + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `āœ… **Tests**: All passed\nšŸ“Š **Coverage**: ${coveragePercentage}` + }); + console.log('Comment sent successfully.'); diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml deleted file mode 100644 index 541a5f5..0000000 --- a/.github/workflows/continuous_integration.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Continuous Integration - -on: - push: - branches: - - main - pull_request: - types: [opened, synchronize, reopened] - -jobs: - sonarcloud: - name: Unit-Tests - runs-on: macos-14 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Java 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - - name: Link SwiftLint or install it - run: brew link --overwrite swiftlint || brew install swiftlint - - - name: Set up XCode - run: sudo xcode-select --switch /Applications/Xcode_15.0.app - - - name: Bundle Install - run: bundle install - - - name: Unit tests - run: bundle exec fastlane unit_tests - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - - - name: Code Coverage - run: bundle exec fastlane coverage - - - name: Lint - run: bundle exec fastlane lint - - - name: Setup sonarqube - uses: warchant/setup-sonar-scanner@v8 - - - name: Send to Sonarcloud - run: bundle exec fastlane sonarqube - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONARCLOUD_KEY }} diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml index edecd2c..eaa2140 100644 --- a/.github/workflows/prepare_release.yml +++ b/.github/workflows/prepare_release.yml @@ -3,79 +3,147 @@ name: Prepare Release on: workflow_dispatch: inputs: - versionBumpLevel: - description: 'Version bump level (patch, minor, major)' - required: true + bump: + description: 'Version bump type (ignored if version is set)' + required: false type: choice - default: 'patch' options: - - patch - - minor - - major + - patch + - minor + - major + version: + description: 'Set specific version (e.g., 1.2.3)' + required: false + +env: + PROJECT_NAME: OSInAppBrowserLib + PBXPROJ: OSInAppBrowserLib.xcodeproj/project.pbxproj + XCODE_VERSION: 16.4 + CHANGELOG_PATH: CHANGELOG.md + LICENSE_PATH: LICENSE + PODSPEC_FILE: OSInAppBrowserLib.podspec jobs: - build-and-release: - if: github.ref == 'refs/heads/main' - runs-on: macos-14 + prepare-release: + name: Prepare Release + runs-on: macos-15 + if: github.ref_name == 'main' + steps: - - name: Checkout + - name: Checkout code uses: actions/checkout@v4 - - - name: Link SwiftLint or install it - run: brew link --overwrite swiftlint || brew install swiftlint - - - name: Set up XCode - run: sudo xcode-select --switch /Applications/Xcode_15.0.app - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3' - - - name: Bump version - run: ruby ./scripts/bump_versions.rb ${{ github.event.inputs.versionBumpLevel }} + fetch-depth: 0 - - name: Build XCFramework - run: ./scripts/build_framework.sh + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + with: + tools: gh - - name: Get new version - id: version - run: echo "VERSION=$(ruby -e 'puts File.read("./OSInAppBrowserLib.podspec").match(/spec.version.*=.*''(\d+\.\d+\.\d+)''/)[1]')" >> $GITHUB_ENV + - name: Get current project version + id: get_version + uses: ./.github/actions/get-project-version + with: + project_name: ${{ env.PROJECT_NAME }} - - name: Create new branch + - name: Bump project version + id: bump_version + env: + CURRENT_VERSION: ${{ steps.get_version.outputs.version }} + INPUT_BUMP: ${{ github.event.inputs.bump }} + INPUT_VERSION: ${{ github.event.inputs.version }} run: | - git switch --create "prepare-new-release-${{ env.VERSION }}" + set -euo pipefail + echo "ā¬†ļø Bumping project version..." + if [ -n "$INPUT_VERSION" ]; then + if ! [[ "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "āŒ Version must be in format M.m.p (e.g., 1.2.3)" + exit 1 + fi + NEW_VERSION="$INPUT_VERSION" + else + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + PATCH="${PATCH:-0}" + case "$INPUT_BUMP" in + major) + MAJOR=$((MAJOR+1)); MINOR=0; PATCH=0;; + minor) + MINOR=$((MINOR+1)); PATCH=0;; + *) + PATCH=$((PATCH+1));; + esac + NEW_VERSION="$MAJOR.$MINOR.$PATCH" + fi + echo "šŸ”¢ New version: $NEW_VERSION" + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - - name: Move zip file to root and push changes + - name: Update project files + env: + PBXPROJ: ${{ env.PBXPROJ }} + PODSPEC_FILE: ${{ env.PODSPEC_FILE }} + CHANGELOG_PATH: ${{ env.CHANGELOG_PATH }} + NEW_VERSION: ${{ steps.bump_version.outputs.new_version }} run: | - if [ -f OSInAppBrowserLib.zip ]; then - rm OSInAppBrowserLib.zip + set -euo pipefail + + echo "šŸ› ļø Updating MARKETING_VERSION..." + sed -i '' -E "s/MARKETING_VERSION = [0-9]+\.[0-9]+(\.[0-9]+)?;/MARKETING_VERSION = $NEW_VERSION;/g" "$PBXPROJ" + echo "šŸ› ļø Updating CURRENT_PROJECT_VERSION..." + current_proj_version=$(grep -m1 'CURRENT_PROJECT_VERSION =' "$PBXPROJ" | sed -E 's/.*CURRENT_PROJECT_VERSION = ([0-9]+);/\1/') + new_proj_version=$((current_proj_version+1)) + sed -i '' -E "s/CURRENT_PROJECT_VERSION = [0-9]+;/CURRENT_PROJECT_VERSION = $new_proj_version;/g" "$PBXPROJ" + echo "āœ… Bumped MARKETING_VERSION to $NEW_VERSION, CURRENT_PROJECT_VERSION to $new_proj_version" + + echo "šŸ› ļø Updating version in $PODSPEC_FILE..." + if [ -f "$PODSPEC_FILE" ]; then + sed -i '' -E "s/^([[:space:]]*[^[:space:]]+\.version[[:space:]]*=[[:space:]]*['\"])[^'\"]+(['\"])/\1$NEW_VERSION\2/" "$PODSPEC_FILE" + echo "āœ… Podspec version updated to $NEW_VERSION" else - echo "File does not exist." + echo "āš ļø Podspec file not found: $PODSPEC_FILE" fi - mv build/OSInAppBrowserLib.zip . - git config --global user.name 'github-actions[bot]' - git config --global user.email 'github-actions[bot]@users.noreply.github.com' - git add . - git commit -m "chore: Bump version to ${{ env.VERSION }}" - git push origin HEAD:prepare-new-release-${{ env.VERSION }} - - name: Create pull request - id: create_pr + echo "šŸ“ Updating $CHANGELOG_PATH..." + TODAY=$(date +%Y-%m-%d) + awk -v ver="$NEW_VERSION" -v today="$TODAY" ' + BEGIN { unreleased_found=0 } + /^## \[Unreleased\]/ { + print $0; print ""; print "## [" ver "] - " today; unreleased_found=1; next + } + { print $0 } + ' "$CHANGELOG_PATH" > "$CHANGELOG_PATH.tmp" && mv "$CHANGELOG_PATH.tmp" "$CHANGELOG_PATH" + echo "āœ… CHANGELOG updated for version $NEW_VERSION" + + - name: Create release branch, commit and push changes + id: create_release_branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.bump_version.outputs.new_version }} run: | - gh pr create -B main -H prepare-new-release-${{ env.VERSION }} --title 'Prepare `main` to Release `${{ env.VERSION }}`' --body 'Bumps version to `${{ env.VERSION }}`.<br/>Creates an updated and ready-to-be-released `OSInAppBrowserLib.zip`.' - PR_NUMBER=$(gh pr view --json number --jq '.number') - echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV + set -euo pipefail + BRANCH_NAME="release/v$VERSION" + git config user.name "github-actions[bot] (on behalf of ${{ github.actor }})" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH_NAME" + git add . + git commit -m "chore(release): prepare release v$VERSION" + git push origin "$BRANCH_NAME" + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: Ensure 'release' label exists env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh label create release --color FFD700 --description "Release PRs" || true - - name: Add label to the pull request - run: | - gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/labels \ - -f "labels[]=release" + - name: Create Pull Request env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.bump_version.outputs.new_version }} + BRANCH_NAME: ${{ steps.create_release_branch.outputs.branch_name }} + run: | + set -euo pipefail + gh pr create \ + --title "Release v$VERSION" \ + --body "Automated PR to release v$VERSION." \ + --head $BRANCH_NAME \ + --base main \ + --label release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9f5e460 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,145 @@ +name: Release + +on: + pull_request: + types: [closed] + branches: + - 'main' + +env: + PROJECT_NAME: OSInAppBrowserLib + XCODE_VERSION: 16.4 + CHANGELOG_PATH: CHANGELOG.md + LICENSE_PATH: LICENSE + PODSPEC_FILE: OSInAppBrowserLib.podspec + +jobs: + tag_and_release: + name: Release and Publish + runs-on: macos-15 + if: >- + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'release') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + with: + tools: gh + gems: cocoapods + + - name: Get current project version + id: get_version + uses: ./.github/actions/get-project-version + with: + project_name: ${{ env.PROJECT_NAME }} + + - name: Get release notes for this version + id: release_notes + env: + VERSION: ${{ steps.get_version.outputs.version }} + CHANGELOG_PATH: ${{ env.CHANGELOG_PATH }} + run: | + set -euo pipefail + if [ ! -f "$CHANGELOG_PATH" ]; then + echo "āŒ Changelog file not found: $CHANGELOG_PATH" + exit 1 + fi + echo "šŸ“œ Extracting release notes for version $VERSION..." + release_notes_section=$(awk "/^## \[${VERSION}\]/ {flag=1; next} flag && /^## \\[/ {exit} flag {print}" "$CHANGELOG_PATH" | sed '/^\s*$/d') + if [ -n "$release_notes_section" ]; then + echo "$release_notes_section" + echo 'release_notes<<EOF' >> $GITHUB_OUTPUT + echo "$release_notes_section" >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + else + echo "āš ļø No release notes found for version $VERSION." + exit 0 + fi + + - name: Set Xcode version + uses: ./.github/actions/set-xcode-version + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Build XCFramework + id: build_xcframework + uses: ./.github/actions/build-xcframework + with: + project_name: ${{ env.PROJECT_NAME }} + + - name: Package XCFramework + uses: ./.github/actions/package-xcframework + id: package + with: + package_name: ${{ env.PROJECT_NAME }} + xcframework_path: ${{ steps.build_xcframework.outputs.xcframework_path }} + license_path: ${{ env.LICENSE_PATH }} + + - name: Create tag + id: create_tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.get_version.outputs.version }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag v$VERSION -m "Release version $VERSION" + git push origin v$VERSION + echo "Tag v$VERSION created." + echo "tag=v$VERSION" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TITLE: ${{ steps.create_tag.outputs.tag }} + RELEASE_NOTES: ${{ steps.release_notes.outputs.release_notes }} + ASSET_PATH: ${{ steps.package.outputs.zip_name }} + run: | + set -euo pipefail + gh release create $TITLE \ + --title "$TITLE" \ + --notes "$RELEASE_NOTES" \ + "$ASSET_PATH" + + - name: Publish Pod + if: hashFiles('${{ env.PODSPEC_FILE }}') != '' + env: + COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + run: | + set -euo pipefail + if [ -z "${COCOAPODS_TRUNK_TOKEN:-}" ]; then + echo "āŒ COCOAPODS_TRUNK_TOKEN secret is not set. Please set it in your repository secrets." + exit 1 + fi + echo "šŸš€ Deploying podspec to CocoaPods..." + pod trunk push "$PODSPEC_FILE" --allow-warnings + + - name: Delete source branch + if: github.event.pull_request.head.ref != 'main' + run: | + set +e + git push origin --delete ${{ github.event.pull_request.head.ref }} + set -e + + delete_branch_if_pr_closed_without_merge: + name: Delete Source Branch If PR Closed Without Merge + runs-on: macos-15 + if: >- + github.event.pull_request.merged == false && + github.event.pull_request.head.ref != 'main' && + contains(github.event.pull_request.labels.*.name, 'release') + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Delete source branch + run: | + set +e + git push origin --delete ${{ github.event.pull_request.head.ref }} + set -e diff --git a/.github/workflows/release_and_publish.yml b/.github/workflows/release_and_publish.yml deleted file mode 100644 index 398321c..0000000 --- a/.github/workflows/release_and_publish.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Release and Publish - -on: - pull_request: - types: [closed] - branches: - - 'main' - -jobs: - post-merge: - if: contains(github.event.pull_request.labels.*.name, 'release') && github.event.pull_request.merged == true - runs-on: macos-14 - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Set up Cocoapods - run: gem install cocoapods - - - name: Get new version - id: version - run: echo "VERSION=$(ruby -e 'puts File.read("./OSInAppBrowserLib.podspec").match(/spec.version.*=.*''(\d+\.\d+\.\d+)''/)[1]')" >> $GITHUB_ENV - - - name: Extract release notes - run: sh scripts/extract_release_notes.sh "${{ env.VERSION }}" >> release_notes.md - - - name: Create Tag - id: create_tag - run: | - # Define the tag name and message - TAG_NAME="${{ env.VERSION }}" - TAG_MESSAGE="Tag for version ${{ env.VERSION }}" - - # Create the tag - git tag -a "$TAG_NAME" -m "$TAG_MESSAGE" - git push origin "$TAG_NAME" - - echo "Tag created: $TAG_NAME" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create Release - run: | - # Extract the tag name - TAG_NAME="${{ env.VERSION }}" - RELEASE_NOTES="$(cat release_notes.md)" - - # Create the release using GitHub CLI - gh release create "$TAG_NAME" \ - --title "$TAG_NAME" \ - --notes "$RELEASE_NOTES" \ - "OSInAppBrowserLib.zip" - - echo "Release created for tag: $TAG_NAME" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Deploy to Cocoapods - run: pod trunk push ./OSInAppBrowserLib.podspec --allow-warnings - env: - COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} - - - name: Delete Release Branch - run: git push origin --delete prepare-new-release-${{ env.VERSION }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 484334d..0000000 --- a/Gemfile +++ /dev/null @@ -1,4 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" -gem "slather" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 68743c5..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,279 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - activesupport (8.0.2) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.4.0) - aws-partitions (1.1116.0) - aws-sdk-core (3.225.2) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - base64 - jmespath (~> 1, >= 1.6.1) - logger - aws-sdk-kms (1.104.0) - aws-sdk-core (~> 3, >= 3.225.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.189.1) - aws-sdk-core (~> 3, >= 3.225.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.12.1) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - base64 (0.3.0) - benchmark (0.4.1) - bigdecimal (3.2.2) - claide (1.1.0) - clamp (1.3.2) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - concurrent-ruby (1.3.5) - connection_pool (2.5.3) - declarative (0.0.20) - digest-crc (0.7.0) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) - dotenv (2.8.1) - drb (2.2.3) - emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.4) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.1.0) - multipart-post (~> 2.0) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.1) - faraday (~> 1.0) - fastimage (2.4.0) - fastlane (2.228.0) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored (~> 1.2) - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - fastlane-sirp (>= 1.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - http-cookie (~> 1.0.5) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (>= 2.0.0, < 3.0.0) - naturally (~> 2.2) - optparse (>= 0.1.1, < 1.0.0) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) - simctl (~> 1.6.3) - terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (~> 3) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.4.1) - xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-sirp (1.0.0) - sysrandom (~> 1.0) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.8.0) - google-cloud-env (>= 1.0, < 3.a) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.5.0) - google-cloud-storage (1.47.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.8) - domain_name (~> 0.5) - httpclient (2.9.0) - mutex_m - i18n (1.14.7) - concurrent-ruby (~> 1.0) - jmespath (1.6.2) - json (2.12.2) - jwt (2.10.1) - base64 - logger (1.7.0) - mini_magick (4.13.2) - mini_mime (1.1.5) - mini_portile2 (2.8.9) - minitest (5.25.5) - multi_json (1.15.0) - multipart-post (2.4.1) - mutex_m (0.3.0) - nanaimo (0.4.0) - naturally (2.3.0) - nkf (0.2.0) - nokogiri (1.18.8) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.8-arm-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.8-arm64-darwin) - racc (~> 1.4) - nokogiri (1.18.8-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-gnu) - racc (~> 1.4) - optparse (0.6.0) - os (1.1.4) - plist (3.7.2) - public_suffix (6.0.2) - racc (1.8.1) - rake (13.3.0) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.4.1) - rouge (3.28.0) - ruby2_keywords (0.0.5) - rubyzip (2.4.1) - securerandom (0.4.1) - security (0.1.5) - signet (0.20.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - slather (2.8.5) - CFPropertyList (>= 2.2, < 4) - activesupport - clamp (~> 1.3) - nokogiri (>= 1.14.3) - xcodeproj (~> 1.27) - sysrandom (1.0.5) - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - uber (0.1.0) - unicode-display_width (2.6.0) - uri (1.0.3) - word_wrap (1.0.0) - xcodeproj (1.27.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - rexml (>= 3.3.6, < 4.0) - xcpretty (0.4.1) - rouge (~> 3.28.0) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - aarch64-linux - arm-linux - arm64-darwin - x86-linux - x86_64-darwin - x86_64-linux - -DEPENDENCIES - fastlane - slather - -BUNDLED WITH - 2.4.19 diff --git a/fastlane/Appfile b/fastlane/Appfile deleted file mode 100644 index 4282947..0000000 --- a/fastlane/Appfile +++ /dev/null @@ -1,6 +0,0 @@ -# app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app -# apple_id("[[APPLE_ID]]") # Your Apple Developer Portal username - - -# For more information about the Appfile, see: -# https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index 0bd62e7..0000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,45 +0,0 @@ -# This file contains the fastlane.tools configuration -# You can find the documentation at https://docs.fastlane.tools -# -# For a list of all available actions, check out -# -# https://docs.fastlane.tools/actions -# -# For a list of all available plugins, check out -# -# https://docs.fastlane.tools/plugins/available-plugins -# - -# Uncomment the line if you want fastlane to automatically update itself -# update_fastlane - -default_platform(:ios) - -platform :ios do - desc "Lane to run the unit tests" - lane :unit_tests do - run_tests(scheme: "OSInAppBrowserLib", - slack_url: ENV['SLACK_WEBHOOK']) - end - - desc "Code coverage" - lane :coverage do - slather( - scheme: "OSInAppBrowserLib", - proj: "OSInAppBrowserLib.xcodeproj", - output_directory: "sonar-reports", - sonarqube_xml: "true" - ) - end - - lane :lint do - swiftlint( - output_file: "sonar-reports/OSInAppBrowserLib-swiftlint.txt", - ignore_exit_status: true - ) - end - - lane :sonarqube do - sonar - end -end From 4b244af91fb0815cadb475f16e22cf9406cabd1e Mon Sep 17 00:00:00 2001 From: Andre Destro <andre.destro@outsystems.com> Date: Wed, 10 Sep 2025 11:26:33 +0100 Subject: [PATCH 2/2] test: add unit tests --- OSInAppBrowserLib.xcodeproj/project.pbxproj | 72 +++++++- .../xcshareddata/swiftpm/Package.resolved | 15 ++ .../xcschemes/OSInAppBrowserLibTests.xcscheme | 55 +++++++ .../OSIABApplicationRouterAdapter.swift | 35 +++- .../OSIABWebViewRouterAdapter.swift | 34 +--- .../WebView/OSIABWebViewModel.swift | 9 +- .../WebView/Views/OSIABNavigationView.swift | 7 +- .../WebView/Views/OSIABWebView.swift | 1 + .../Views/OSIABWebView13WrapperView.swift | 104 ------------ .../Views/OSIABWebViewWrapperView.swift | 64 +++----- .../OSIABApplicationRouterAdapterTests.swift | 62 +++++++ .../WebView/Views/OSIABErrorViewTests.swift | 49 ++++++ .../Views/OSIABNavigationViewTests.swift | 120 ++++++++++++++ .../OSIABWebViewRepresentableTests.swift | 12 ++ .../WebView/Views/OSIABWebViewTests.swift | 155 ++++++++++++++++++ 15 files changed, 596 insertions(+), 198 deletions(-) create mode 100644 OSInAppBrowserLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 OSInAppBrowserLib.xcodeproj/xcshareddata/xcschemes/OSInAppBrowserLibTests.xcscheme delete mode 100644 Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView13WrapperView.swift create mode 100644 Tests/OSInAppBrowserLibTests/OSIABApplicationRouterAdapterTests.swift create mode 100644 Tests/OSInAppBrowserLibTests/WebView/Views/OSIABErrorViewTests.swift create mode 100644 Tests/OSInAppBrowserLibTests/WebView/Views/OSIABNavigationViewTests.swift create mode 100644 Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewRepresentableTests.swift create mode 100644 Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewTests.swift diff --git a/OSInAppBrowserLib.xcodeproj/project.pbxproj b/OSInAppBrowserLib.xcodeproj/project.pbxproj index cc4d6bd..d915de2 100644 --- a/OSInAppBrowserLib.xcodeproj/project.pbxproj +++ b/OSInAppBrowserLib.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 1BB4F8BC2C36F54800F50A33 /* OSIABErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BB4F8BB2C36F54800F50A33 /* OSIABErrorView.swift */; }; - 7506BDD22C622B8D00409255 /* OSIABWebView13WrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7506BDD12C622B8D00409255 /* OSIABWebView13WrapperView.swift */; }; 750949C02C11D992006843E1 /* OSIABToolbarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750949BE2C11D992006843E1 /* OSIABToolbarPosition.swift */; }; 750949C12C11D992006843E1 /* OSIABWebViewOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750949BF2C11D992006843E1 /* OSIABWebViewOptions.swift */; }; 750949C32C11D9D0006843E1 /* OSIABWebViewRouterAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750949C22C11D9D0006843E1 /* OSIABWebViewRouterAdapter.swift */; }; @@ -40,6 +39,12 @@ 75EF1AFB2C13069E005D7164 /* OSIABViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EF1AFA2C13069E005D7164 /* OSIABViewModelTests.swift */; }; 75EF1AFD2C1306B1005D7164 /* OSIABCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EF1AFC2C1306B1005D7164 /* OSIABCacheManagerTests.swift */; }; 75EF1B012C134878005D7164 /* OSIABWebViewStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EF1B002C134878005D7164 /* OSIABWebViewStub.swift */; }; + 887287832E717E8F00D9E41B /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 887287822E717E8F00D9E41B /* ViewInspector */; }; + 8872878B2E71806200D9E41B /* OSIABWebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872878A2E71806200D9E41B /* OSIABWebViewTests.swift */; }; + 8872878F2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872878E2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift */; }; + 887287912E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887287902E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift */; }; + 887287932E7196C800D9E41B /* OSIABErrorViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887287922E7196C800D9E41B /* OSIABErrorViewTests.swift */; }; + 887287992E71C2E100D9E41B /* OSIABNavigationViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887287982E71C2E100D9E41B /* OSIABNavigationViewTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,7 +59,6 @@ /* Begin PBXFileReference section */ 1BB4F8BB2C36F54800F50A33 /* OSIABErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABErrorView.swift; sourceTree = "<group>"; }; - 7506BDD12C622B8D00409255 /* OSIABWebView13WrapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABWebView13WrapperView.swift; sourceTree = "<group>"; }; 750949BE2C11D992006843E1 /* OSIABToolbarPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSIABToolbarPosition.swift; sourceTree = "<group>"; }; 750949BF2C11D992006843E1 /* OSIABWebViewOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSIABWebViewOptions.swift; sourceTree = "<group>"; }; 750949C22C11D9D0006843E1 /* OSIABWebViewRouterAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSIABWebViewRouterAdapter.swift; sourceTree = "<group>"; }; @@ -87,6 +91,11 @@ 75EF1AFA2C13069E005D7164 /* OSIABViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABViewModelTests.swift; sourceTree = "<group>"; }; 75EF1AFC2C1306B1005D7164 /* OSIABCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABCacheManagerTests.swift; sourceTree = "<group>"; }; 75EF1B002C134878005D7164 /* OSIABWebViewStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABWebViewStub.swift; sourceTree = "<group>"; }; + 8872878A2E71806200D9E41B /* OSIABWebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABWebViewTests.swift; sourceTree = "<group>"; }; + 8872878E2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABWebViewRepresentableTests.swift; sourceTree = "<group>"; }; + 887287902E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABApplicationRouterAdapterTests.swift; sourceTree = "<group>"; }; + 887287922E7196C800D9E41B /* OSIABErrorViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABErrorViewTests.swift; sourceTree = "<group>"; }; + 887287982E71C2E100D9E41B /* OSIABNavigationViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIABNavigationViewTests.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -102,6 +111,7 @@ buildActionMask = 2147483647; files = ( 7575CF6A2BFCEE6F008F3FD0 /* OSInAppBrowserLib.framework in Frameworks */, + 887287832E717E8F00D9E41B /* ViewInspector in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -126,7 +136,6 @@ 759027B22C1AFBCB00E02498 /* OSIABNavigationView.swift */, 750949C52C11D9E2006843E1 /* OSIABWebView.swift */, 750949C92C11D9E2006843E1 /* OSIABWebViewRepresentable.swift */, - 7506BDD12C622B8D00409255 /* OSIABWebView13WrapperView.swift */, 750949C72C11D9E2006843E1 /* OSIABWebViewWrapperView.swift */, ); path = Views; @@ -209,6 +218,8 @@ 7575CF6D2BFCEE6F008F3FD0 /* OSInAppBrowserLibTests */ = { isa = PBXGroup; children = ( + 887287902E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift */, + 8872877F2E717C4600D9E41B /* WebView */, 7575CF7E2BFCEEEA008F3FD0 /* Helper Files */, 75EF1AFC2C1306B1005D7164 /* OSIABCacheManagerTests.swift */, 7575CF7D2BFCEEEA008F3FD0 /* OSIABEngineTests.swift */, @@ -230,6 +241,25 @@ path = "Helper Files"; sourceTree = "<group>"; }; + 8872877E2E717C4600D9E41B /* Views */ = { + isa = PBXGroup; + children = ( + 887287982E71C2E100D9E41B /* OSIABNavigationViewTests.swift */, + 887287922E7196C800D9E41B /* OSIABErrorViewTests.swift */, + 8872878A2E71806200D9E41B /* OSIABWebViewTests.swift */, + 8872878E2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift */, + ); + path = Views; + sourceTree = "<group>"; + }; + 8872877F2E717C4600D9E41B /* WebView */ = { + isa = PBXGroup; + children = ( + 8872877E2E717C4600D9E41B /* Views */, + ); + path = WebView; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -309,6 +339,9 @@ Base, ); mainGroup = 7575CF572BFCEE6F008F3FD0; + packageReferences = ( + 887287812E717E8F00D9E41B /* XCRemoteSwiftPackageReference "ViewInspector" */, + ); productRefGroup = 7575CF622BFCEE6F008F3FD0 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -371,7 +404,6 @@ 750949C02C11D992006843E1 /* OSIABToolbarPosition.swift in Sources */, 750949CA2C11D9E2006843E1 /* OSIABCacheManager.swift in Sources */, 759027B72C1B13CA00E02498 /* OSIABWebViewUIModel.swift in Sources */, - 7506BDD22C622B8D00409255 /* OSIABWebView13WrapperView.swift in Sources */, 754451F62C04AB69003D7361 /* OSIABViewStyle.swift in Sources */, 756346642C00F21000685AA3 /* OSIABApplicationRouterAdapter.swift in Sources */, 750949CF2C11D9E2006843E1 /* OSIABWebViewRepresentable.swift in Sources */, @@ -394,12 +426,17 @@ buildActionMask = 2147483647; files = ( 756346482C00DD4700685AA3 /* OSIABSafariViewControllerRouterAdapterTests.swift in Sources */, + 8872878F2E7192C200D9E41B /* OSIABWebViewRepresentableTests.swift in Sources */, + 8872878B2E71806200D9E41B /* OSIABWebViewTests.swift in Sources */, 75EF1AFB2C13069E005D7164 /* OSIABViewModelTests.swift in Sources */, 7575CF812BFCEEEA008F3FD0 /* OSIABRouterSpy.swift in Sources */, 75094A072C121BD9006843E1 /* OSIABCacheManagerStub.swift in Sources */, + 887287932E7196C800D9E41B /* OSIABErrorViewTests.swift in Sources */, + 887287992E71C2E100D9E41B /* OSIABNavigationViewTests.swift in Sources */, 75EF1AFD2C1306B1005D7164 /* OSIABCacheManagerTests.swift in Sources */, 7575CF802BFCEEEA008F3FD0 /* OSIABEngineTests.swift in Sources */, 75EF1B012C134878005D7164 /* OSIABWebViewStub.swift in Sources */, + 887287912E7194D800D9E41B /* OSIABApplicationRouterAdapterTests.swift in Sources */, 75094A052C1213A3006843E1 /* OSIABWebViewRouterAdapterTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -559,7 +596,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -593,7 +630,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -619,7 +656,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 5; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 2.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLibTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -641,7 +678,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 5; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 2.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.inappbrowser.OSInAppBrowserLibTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -685,6 +722,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 887287812E717E8F00D9E41B /* XCRemoteSwiftPackageReference "ViewInspector" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/nalexn/ViewInspector"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.10.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 887287822E717E8F00D9E41B /* ViewInspector */ = { + isa = XCSwiftPackageProductDependency; + package = 887287812E717E8F00D9E41B /* XCRemoteSwiftPackageReference "ViewInspector" */; + productName = ViewInspector; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 7575CF582BFCEE6F008F3FD0 /* Project object */; } diff --git a/OSInAppBrowserLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OSInAppBrowserLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..267a91d --- /dev/null +++ b/OSInAppBrowserLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "7abf1dd76d62709d50ab97ba7b5907de6a95cbc813d2bf154db0688b92d85914", + "pins" : [ + { + "identity" : "viewinspector", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nalexn/ViewInspector", + "state" : { + "revision" : "a6fcac8485bc8f57b2d2b55bb6d97138e8659e4b", + "version" : "0.10.2" + } + } + ], + "version" : 3 +} diff --git a/OSInAppBrowserLib.xcodeproj/xcshareddata/xcschemes/OSInAppBrowserLibTests.xcscheme b/OSInAppBrowserLib.xcodeproj/xcshareddata/xcschemes/OSInAppBrowserLibTests.xcscheme new file mode 100644 index 0000000..4780f54 --- /dev/null +++ b/OSInAppBrowserLib.xcodeproj/xcshareddata/xcschemes/OSInAppBrowserLibTests.xcscheme @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1640" + version = "1.7"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + <Testables> + <TestableReference + skipped = "NO" + parallelizable = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "7575CF682BFCEE6F008F3FD0" + BuildableName = "OSInAppBrowserLibTests.xctest" + BlueprintName = "OSInAppBrowserLibTests" + ReferencedContainer = "container:OSInAppBrowserLib.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Sources/OSInAppBrowserLib/RouterAdapters/OSIABApplicationRouterAdapter.swift b/Sources/OSInAppBrowserLib/RouterAdapters/OSIABApplicationRouterAdapter.swift index 2c54194..67b48f1 100644 --- a/Sources/OSInAppBrowserLib/RouterAdapters/OSIABApplicationRouterAdapter.swift +++ b/Sources/OSInAppBrowserLib/RouterAdapters/OSIABApplicationRouterAdapter.swift @@ -1,14 +1,35 @@ import UIKit -/// Adapter that makes the required calls so that can perform the External Browser routing. -public class OSIABApplicationRouterAdapter: OSIABRouter { - public typealias ReturnType = Bool - - /// Constructor method. +public protocol URLOpener { + func canOpenURL(_ url: URL) -> Bool + func open(_ url: URL, completionHandler: ((Bool) -> Void)?) +} + +public class DefaultURLOpener: URLOpener { public init() {} + public func canOpenURL(_ url: URL) -> Bool { + UIApplication.shared.canOpenURL(url) + } + + public func open(_ url: URL, completionHandler: ((Bool) -> Void)?) { + UIApplication.shared.open(url, completionHandler: completionHandler ?? { _ in }) + } +} + +public class OSIABApplicationRouterAdapter: OSIABRouter { + public typealias ReturnType = Bool + + private let urlOpener: URLOpener + + public init(urlOpener: URLOpener = DefaultURLOpener()) { + self.urlOpener = urlOpener + } + public func handleOpen(_ url: URL, _ completionHandler: @escaping (ReturnType) -> Void) { - guard UIApplication.shared.canOpenURL(url) else { return completionHandler(false) } - UIApplication.shared.open(url, completionHandler: completionHandler) + guard urlOpener.canOpenURL(url) else { + return completionHandler(false) + } + urlOpener.open(url, completionHandler: completionHandler) } } diff --git a/Sources/OSInAppBrowserLib/RouterAdapters/OSIABWebViewRouterAdapter.swift b/Sources/OSInAppBrowserLib/RouterAdapters/OSIABWebViewRouterAdapter.swift index 0a5ab4b..e8fe3ba 100644 --- a/Sources/OSInAppBrowserLib/RouterAdapters/OSIABWebViewRouterAdapter.swift +++ b/Sources/OSInAppBrowserLib/RouterAdapters/OSIABWebViewRouterAdapter.swift @@ -53,11 +53,7 @@ public class OSIABWebViewRouterAdapter: NSObject, OSIABRouter { let dismissCallback: () -> Void = { self.callbackHandler.onBrowserClosed(true) } let hostingController: UIViewController - if #available(iOS 14.0, *) { - hostingController = OSIABWebViewController(rootView: .init(viewModel), dismiss: dismissCallback) - } else { - hostingController = OSIABWebView13Controller(rootView: .init(viewModel), dismiss: dismissCallback) - } + hostingController = OSIABWebViewController(rootView: .init(viewModel), dismiss: dismissCallback) hostingController.modalPresentationStyle = options.modalPresentationStyle hostingController.modalTransitionStyle = options.modalTransitionStyle hostingController.presentationController?.delegate = self @@ -127,31 +123,3 @@ private class OSIABWebViewController: UIHostingController<OSIABWebViewWrapperVie }) } } - -/// A subclass for `UIHostingController` where it's possible to delegate the `dismiss` call to its callers. -@available(iOS, deprecated: 14.0, message: "Use OSIABWebViewController for iOS 14.0+") -private class OSIABWebView13Controller: UIHostingController<OSIABWebView13WrapperView> { - /// Callback to trigger when the view controller is closed. - let dismiss: (() -> Void)? - - /// Constructor method. - /// - Parameters: - /// - rootView: The root view of the SwiftUI view hierarchy that you want to manage using the hosting view controller. - /// - dismiss: The callback to trigger when the view controller is dismissed. - init(rootView: OSIABWebView13WrapperView, dismiss: (() -> Void)?) { - self.dismiss = dismiss - super.init(rootView: rootView) - } - - @MainActor required dynamic init?(coder aDecoder: NSCoder) { - self.dismiss = nil - super.init(coder: aDecoder) - } - - override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - super.dismiss(animated: flag, completion: { - self.dismiss?() - completion?() - }) - } -} diff --git a/Sources/OSInAppBrowserLib/WebView/OSIABWebViewModel.swift b/Sources/OSInAppBrowserLib/WebView/OSIABWebViewModel.swift index a4323bd..91322ee 100644 --- a/Sources/OSInAppBrowserLib/WebView/OSIABWebViewModel.swift +++ b/Sources/OSInAppBrowserLib/WebView/OSIABWebViewModel.swift @@ -38,7 +38,14 @@ class OSIABWebViewModel: NSObject, ObservableObject { @Published private(set) var addressLabel: String = "" private var cancellables = Set<AnyCancellable>() - + + #if DEBUG + /// Test-only method to set error for unit tests + func setErrorForTesting(_ error: Error?) { + self.error = error + } + #endif + /// Constructor method. /// - Parameters: /// - url: The current URL being displayed diff --git a/Sources/OSInAppBrowserLib/WebView/Views/OSIABNavigationView.swift b/Sources/OSInAppBrowserLib/WebView/Views/OSIABNavigationView.swift index e9f8e31..bce82d1 100644 --- a/Sources/OSInAppBrowserLib/WebView/Views/OSIABNavigationView.swift +++ b/Sources/OSInAppBrowserLib/WebView/Views/OSIABNavigationView.swift @@ -1,12 +1,12 @@ import SwiftUI -private struct OSIABNavigationButton: View { +internal struct OSIABNavigationButton: View { /// Handler to trigger when the button is pressed. private let buttonPressed: () -> Void /// The icon to set the button with. private let iconName: String /// Indicates if the button should appeared as enabled or not. - private let isDisabled: Bool + internal let isDisabled: Bool /// Constructor method. /// - Parameters: @@ -88,7 +88,7 @@ struct OSIABNavigationView: View { OSIABNavigationButton( forwardButtonPressed, - iconName: "chevron.forward", + iconName: "chevron.forward", isDisabled: !forwardButtonEnabled ) } @@ -151,6 +151,7 @@ private struct OSIABTestNavigationView: View { } } +@available(*, unavailable) struct OSIABNavigationView_Previews: PreviewProvider { static var previews: some View { // Default - Light Mode diff --git a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView.swift b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView.swift index 161147d..6d5f389 100644 --- a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView.swift +++ b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView.swift @@ -151,6 +151,7 @@ private struct OSIABTestWebView: View { } } +@available(*, unavailable) struct OSIABWebView_Previews: PreviewProvider { static var previews: some View { // MARK: - Default Views diff --git a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView13WrapperView.swift b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView13WrapperView.swift deleted file mode 100644 index fba1aa9..0000000 --- a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebView13WrapperView.swift +++ /dev/null @@ -1,104 +0,0 @@ -import SwiftUI -import UIKit - -/// View that manages which view to present, depending if the page load was successful or not or is being loaded. -@available(iOS, deprecated: 14.0, message: "Use OSIABWebViewWrapperView for iOS 14.0+") -struct OSIABWebView13WrapperView: View { - /// View Model containing all the customisable elements. - @ObservedObject private var model: OSIABWebViewModel - - /// Constructor method. - /// - Parameter model: View Model containing all the customisable elements. - init(_ model: OSIABWebViewModel) { - self._model = ObservedObject(wrappedValue: model) - } - - var body: some View { - ZStack { - OSIABWebView(model) - .edgesIgnoringSafeArea(model.toolbarPosition == .bottom ? [] : .bottom) - if model.isLoading { - OSIABActivityIndicator(isAnimating: .constant(true), style: .large) - } - } - } -} - -private struct OSIABActivityIndicator: UIViewRepresentable { - @Binding private var isAnimating: Bool - private let style: UIActivityIndicatorView.Style - - init(isAnimating: Binding<Bool>, style: UIActivityIndicatorView.Style) { - self._isAnimating = isAnimating - self.style = style - } - - func makeUIView(context: UIViewRepresentableContext<OSIABActivityIndicator>) -> UIActivityIndicatorView { - return UIActivityIndicatorView(style: style) - } - - func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<OSIABActivityIndicator>) { - if self.isAnimating { - uiView.startAnimating() - } else { - uiView.stopAnimating() - } - } -} - -// MARK: - OSIABViewModel's constructor accelerator. -private extension OSIABWebViewModel { - convenience init(toolbarPosition: OSIABToolbarPosition = .defaultValue) { - let configurationModel = OSIABWebViewConfigurationModel() - self.init( - url: .init(string: "https://outsystems.com")!, - webViewConfiguration: configurationModel.toWebViewConfiguration(), - uiModel: .init(toolbarPosition: toolbarPosition), - callbackHandler: .init( - onDelegateURL: { _ in }, - onDelegateAlertController: { _ in }, - onBrowserPageLoad: {}, - onBrowserClosed: { _ in }, - onBrowserPageNavigationCompleted: { _ in } - ) - ) - } - - convenience init(url: String) { - let configurationModel = OSIABWebViewConfigurationModel() - self.init( - url: .init(string: url)!, - webViewConfiguration: configurationModel.toWebViewConfiguration(), - uiModel: .init(), - callbackHandler: .init( - onDelegateURL: { _ in }, - onDelegateAlertController: { _ in }, - onBrowserPageLoad: {}, - onBrowserClosed: { _ in }, - onBrowserPageNavigationCompleted: { _ in } - ) - ) - } -} - -struct OSIABWebView13WrapperView_Previews: PreviewProvider { - static var previews: some View { - - // Default - Light Mode - OSIABWebView13WrapperView(.init()) - - // Default - Dark Mode - OSIABWebView13WrapperView(.init()) - .preferredColorScheme(.dark) - - // Bottom Toolbar Defined - OSIABWebView13WrapperView(.init(toolbarPosition: .bottom)) - - // Error View - Light mode - OSIABWebView13WrapperView(.init(url: "https://outsystems/")) - - // Error View - Dark mode - OSIABWebView13WrapperView(.init(url: "https://outsystems/")) - .preferredColorScheme(.dark) - } -} diff --git a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebViewWrapperView.swift b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebViewWrapperView.swift index 40adc94..75fdcff 100644 --- a/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebViewWrapperView.swift +++ b/Sources/OSInAppBrowserLib/WebView/Views/OSIABWebViewWrapperView.swift @@ -1,7 +1,6 @@ import SwiftUI /// View that manages which view to present, depending if the page load was successful or not or is being loaded. -@available(iOS 14.0, *) struct OSIABWebViewWrapperView: View { /// View Model containing all the customisable elements. @StateObject private var model: OSIABWebViewModel @@ -23,59 +22,40 @@ struct OSIABWebViewWrapperView: View { } } -// MARK: - OSIABViewModel's constructor accelerator. -private extension OSIABWebViewModel { - convenience init(toolbarPosition: OSIABToolbarPosition = .defaultValue) { - let configurationModel = OSIABWebViewConfigurationModel() - self.init( - url: .init(string: "https://outsystems.com")!, - webViewConfiguration: configurationModel.toWebViewConfiguration(), - uiModel: .init(toolbarPosition: toolbarPosition), - callbackHandler: .init( - onDelegateURL: { _ in }, - onDelegateAlertController: { _ in }, - onBrowserPageLoad: {}, - onBrowserClosed: { _ in }, - onBrowserPageNavigationCompleted: { _ in } - ) - ) - } - - convenience init(url: String) { - let configurationModel = OSIABWebViewConfigurationModel() - self.init( - url: .init(string: url)!, - webViewConfiguration: configurationModel.toWebViewConfiguration(), - uiModel: .init(), - callbackHandler: .init( - onDelegateURL: { _ in }, - onDelegateAlertController: { _ in }, - onBrowserPageLoad: {}, - onBrowserClosed: { _ in }, - onBrowserPageNavigationCompleted: { _ in } - ) - ) - } -} - -@available(iOS 14.0, *) +@available(*, unavailable) struct OSIABWebViewWrapperView_Previews: PreviewProvider { static var previews: some View { // Default - Light Mode - OSIABWebViewWrapperView(.init()) + OSIABWebViewWrapperView(makeModel()) // Default - Dark Mode - OSIABWebViewWrapperView(.init()) + OSIABWebViewWrapperView(makeModel()) .preferredColorScheme(.dark) // Bottom Toolbar Defined - OSIABWebViewWrapperView(.init(toolbarPosition: .bottom)) + OSIABWebViewWrapperView(makeModel(toolbarPosition: .bottom)) // Error View - Light mode - OSIABWebViewWrapperView(.init(url: "https://outsystems/")) + OSIABWebViewWrapperView(makeModel(url: "https://outsystems/")) // Error View - Dark mode - OSIABWebViewWrapperView(.init(url: "https://outsystems/")) + OSIABWebViewWrapperView(makeModel(url: "https://outsystems/")) .preferredColorScheme(.dark) } + + private static func makeModel(url: String = "https://outsystems.com", toolbarPosition: OSIABToolbarPosition = .defaultValue) -> OSIABWebViewModel { + let configurationModel = OSIABWebViewConfigurationModel() + return .init( + url: .init(string: url)!, + webViewConfiguration: configurationModel.toWebViewConfiguration(), + uiModel: .init(toolbarPosition: toolbarPosition), + callbackHandler: .init( + onDelegateURL: { _ in }, + onDelegateAlertController: { _ in }, + onBrowserPageLoad: {}, + onBrowserClosed: { _ in }, + onBrowserPageNavigationCompleted: { _ in } + ) + ) + } } diff --git a/Tests/OSInAppBrowserLibTests/OSIABApplicationRouterAdapterTests.swift b/Tests/OSInAppBrowserLibTests/OSIABApplicationRouterAdapterTests.swift new file mode 100644 index 0000000..a38ac3a --- /dev/null +++ b/Tests/OSInAppBrowserLibTests/OSIABApplicationRouterAdapterTests.swift @@ -0,0 +1,62 @@ +import XCTest +@testable import OSInAppBrowserLib + +class MockURLOpener: URLOpener { + var canOpenURLCalled = false + var openCalled = false + + var canOpenURLResult = true + var lastOpenedURL: URL? + var completionResult = true + + func canOpenURL(_ url: URL) -> Bool { + canOpenURLCalled = true + return canOpenURLResult + } + + func open(_ url: URL, completionHandler: ((Bool) -> Void)?) { + openCalled = true + lastOpenedURL = url + completionHandler?(completionResult) + } +} + +final class OSIABApplicationRouterAdapterTests: XCTestCase { + + func test_handleOpen_whenCanOpenURLIsFalse_shouldCallCompletionWithFalse() { + let mock = MockURLOpener() + mock.canOpenURLResult = false + let sut = OSIABApplicationRouterAdapter(urlOpener: mock) + + let expectation = XCTestExpectation(description: "Completion called") + + sut.handleOpen(URL(string: "https://example.com")!) { success in + XCTAssertFalse(success) + XCTAssertTrue(mock.canOpenURLCalled) + XCTAssertFalse(mock.openCalled) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func test_handleOpen_whenCanOpenURLIsTrue_shouldOpenURL() { + let mock = MockURLOpener() + mock.canOpenURLResult = true + mock.completionResult = true + let sut = OSIABApplicationRouterAdapter(urlOpener: mock) + + let url = URL(string: "https://example.com")! + let expectation = XCTestExpectation(description: "Completion called") + + sut.handleOpen(url) { success in + XCTAssertTrue(success) + XCTAssertTrue(mock.canOpenURLCalled) + XCTAssertTrue(mock.openCalled) + XCTAssertEqual(mock.lastOpenedURL, url) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } +} diff --git a/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABErrorViewTests.swift b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABErrorViewTests.swift new file mode 100644 index 0000000..c43e042 --- /dev/null +++ b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABErrorViewTests.swift @@ -0,0 +1,49 @@ +import XCTest +import SwiftUI +import ViewInspector +@testable import OSInAppBrowserLib + +extension OSIABErrorView: Inspectable {} + +final class OSIABErrorViewTests: XCTestCase { + func testShowsErrorMessage() throws { + let view = OSIABErrorView( + NSError(domain: "Test", code: 1, userInfo: nil), + reload: {}, + reloadViewLayoutDirection: .fixed(value: .leftToRight) + ) + let vStack = try view.inspect().vStack() + let text = try vStack.text(0).string() + XCTAssertEqual(text, "Couldn't load the page content.") + } + + func testReloadButtonAction() throws { + var reloaded = false + let view = OSIABErrorView( + NSError(domain: "Test", code: 1, userInfo: nil), + reload: { reloaded = true }, + reloadViewLayoutDirection: .fixed(value: .leftToRight) + ) + let vStack = try view.inspect().vStack() + let hStack = try vStack.hStack(1) + let button = try hStack.button(0) + try button.tap() + XCTAssertTrue(reloaded) + } + + func testReloadButtonLabel() throws { + let view = OSIABErrorView( + NSError(domain: "Test", code: 1, userInfo: nil), + reload: {}, + reloadViewLayoutDirection: .fixed(value: .leftToRight) + ) + let vStack = try view.inspect().vStack() + let hStack = try vStack.hStack(1) + let button = try hStack.button(0) + let labelHStack = try button.labelView().hStack() + let imageName = try labelHStack.image(0).actualImage().name() + let text = try labelHStack.text(1).string() + XCTAssertEqual(imageName, "arrow.clockwise") + XCTAssertEqual(text, "Reload page") + } +} diff --git a/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABNavigationViewTests.swift b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABNavigationViewTests.swift new file mode 100644 index 0000000..0ae1a40 --- /dev/null +++ b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABNavigationViewTests.swift @@ -0,0 +1,120 @@ +import XCTest +import SwiftUI +import ViewInspector +@testable import OSInAppBrowserLib + +extension OSIABNavigationView: Inspectable {} + +final class OSIABNavigationViewTests: XCTestCase { + func testNavigationButtonsAreVisibleWhenEnabled() throws { + let view = OSIABNavigationView( + showNavigationButtons: true, + backButtonPressed: {}, + backButtonEnabled: true, + forwardButtonPressed: {}, + forwardButtonEnabled: true, + addressLabel: "Test URL", + addressLabelAlignment: .trailing, + buttonLayoutDirection: .fixed(value: .leftToRight) + ) + let hStack = try view.inspect().hStack() + XCTAssertNoThrow(try hStack.find(OSIABNavigationButton.self)) + } + + func testNavigationButtonsAreHiddenWhenDisabled() throws { + let view = OSIABNavigationView( + showNavigationButtons: false, + backButtonPressed: {}, + backButtonEnabled: true, + forwardButtonPressed: {}, + forwardButtonEnabled: true, + addressLabel: "Test URL", + addressLabelAlignment: .trailing, + buttonLayoutDirection: .fixed(value: .leftToRight) + ) + let hStack = try view.inspect().hStack() + XCTAssertThrowsError(try hStack.find(OSIABNavigationButton.self)) + } + + func testBackButtonIsDisabled() throws { + let view = OSIABNavigationView( + showNavigationButtons: true, + backButtonPressed: {}, + backButtonEnabled: false, + forwardButtonPressed: {}, + forwardButtonEnabled: true, + addressLabel: "Test URL", + addressLabelAlignment: .trailing, + buttonLayoutDirection: .fixed(value: .leftToRight) + ) + let backButton = try view.inspect().findAll(OSIABNavigationButton.self)[0] + let isDisabled = try backButton.actualView().isDisabled + XCTAssertTrue(isDisabled) + } + + func testForwardButtonIsDisabled() throws { + let view = OSIABNavigationView( + showNavigationButtons: true, + backButtonPressed: {}, + backButtonEnabled: true, + forwardButtonPressed: {}, + forwardButtonEnabled: false, + addressLabel: "Test URL", + addressLabelAlignment: .trailing, + buttonLayoutDirection: .fixed(value: .leftToRight) + ) + let forwardButton = try view.inspect().findAll(OSIABNavigationButton.self)[1] + let isDisabled = try forwardButton.actualView().isDisabled + XCTAssertTrue(isDisabled) + } + + func testAddressLabelIsVisibleAndCorrect() throws { + let label = "Test URL" + let view = OSIABNavigationView( + showNavigationButtons: false, + backButtonPressed: {}, + backButtonEnabled: true, + forwardButtonPressed: {}, + forwardButtonEnabled: true, + addressLabel: label, + addressLabelAlignment: .center, + buttonLayoutDirection: .fixed(value: .leftToRight) + ) + let text = try view.inspect().find(ViewType.Text.self).string() + XCTAssertEqual(text, label) + } + + func testBackButtonActionIsCalled() throws { + let exp = expectation(description: "Back button pressed") + let view = OSIABNavigationView( + showNavigationButtons: true, + backButtonPressed: { exp.fulfill() }, + backButtonEnabled: true, + forwardButtonPressed: {}, + forwardButtonEnabled: true, + addressLabel: "Test URL", + addressLabelAlignment: .trailing, + buttonLayoutDirection: .fixed(value: .leftToRight) + ) + let backButton = try view.inspect().findAll(OSIABNavigationButton.self)[0] + try backButton.button().tap() + wait(for: [exp], timeout: 1) + } + + func testForwardButtonActionIsCalled() throws { + let exp = expectation(description: "Forward button pressed") + let view = OSIABNavigationView( + showNavigationButtons: true, + backButtonPressed: {}, + backButtonEnabled: true, + forwardButtonPressed: { exp.fulfill() }, + forwardButtonEnabled: true, + addressLabel: "Test URL", + addressLabelAlignment: .trailing, + buttonLayoutDirection: .fixed(value: .leftToRight) + ) + let forwardButton = try view.inspect().findAll(OSIABNavigationButton.self)[1] + try forwardButton.button().tap() + wait(for: [exp], timeout: 1) + } +} diff --git a/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewRepresentableTests.swift b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewRepresentableTests.swift new file mode 100644 index 0000000..87eb8d6 --- /dev/null +++ b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewRepresentableTests.swift @@ -0,0 +1,12 @@ +import XCTest +import WebKit +@testable import OSInAppBrowserLib + +final class OSIABWebViewRepresentableTests: XCTestCase { + + func testInitializerStoresInjectedWebView() { + let webView = WKWebView() + let representable = OSIABWebViewRepresentable(webView) + XCTAssertNotNil(representable) + } +} diff --git a/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewTests.swift b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewTests.swift new file mode 100644 index 0000000..f9f2241 --- /dev/null +++ b/Tests/OSInAppBrowserLibTests/WebView/Views/OSIABWebViewTests.swift @@ -0,0 +1,155 @@ +import XCTest +import ViewInspector +@testable import OSInAppBrowserLib + +final class OSIABWebViewTests: XCTestCase { + func testToolbarAppearsAtTop() throws { + let model = OSIABWebViewModel( + url: URL(string: "https://test.com")!, + webViewConfiguration: .init(), + uiModel: .init( + showURL: true, + showToolbar: true, + toolbarPosition: .top, + showNavigationButtons: true, + leftToRight: false, + closeButtonText: "Close" + ), + callbackHandler: .init( + onDelegateURL: { _ in }, + onDelegateAlertController: { _ in }, + onBrowserPageLoad: {}, + onBrowserClosed: { _ in }, + onBrowserPageNavigationCompleted: { _ in } + ) + ) + let view = OSIABWebView(model) + let vStack = try view.inspect().vStack() + // Toolbar should be the first element + XCTAssertNoThrow(try vStack.hStack(0)) + } + + func testToolbarAppearsAtBottom() throws { + let model = OSIABWebViewModel( + url: URL(string: "https://test.com")!, + webViewConfiguration: .init(), + uiModel: .init( + showURL: true, + showToolbar: true, + toolbarPosition: .bottom, + showNavigationButtons: true, + leftToRight: false, + closeButtonText: "Close" + ), + callbackHandler: .init( + onDelegateURL: { _ in }, + onDelegateAlertController: { _ in }, + onBrowserPageLoad: {}, + onBrowserClosed: { _ in }, + onBrowserPageNavigationCompleted: { _ in } + ) + ) + let view = OSIABWebView(model) + let vStack = try view.inspect().vStack() + // Toolbar should be the last element + XCTAssertNoThrow(try vStack.view(OSIABNavigationView.self, 2)) + } + + func testErrorViewAppears() throws { + let model = OSIABWebViewModel( + url: URL(string: "https://test.com")!, + webViewConfiguration: .init(), + uiModel: .init( + showURL: true, + showToolbar: true, + toolbarPosition: .top, + showNavigationButtons: true, + leftToRight: false, + closeButtonText: "Close" + ), + callbackHandler: .init( + onDelegateURL: { _ in }, + onDelegateAlertController: { _ in }, + onBrowserPageLoad: {}, + onBrowserClosed: { _ in }, + onBrowserPageNavigationCompleted: { _ in } + ) + ) + // Simulate error using test-only method + model.setErrorForTesting(NSError(domain: "Test", code: 1, userInfo: nil)) + let view = OSIABWebView(model) + let vStack = try view.inspect().vStack() + // Error view should be present + XCTAssertNoThrow(try vStack.view(OSIABErrorView.self, 1)) + } + + func testCloseButtonAction() throws { + var closed = false + let model = OSIABWebViewModel( + url: URL(string: "https://test.com")!, + webViewConfiguration: .init(), + uiModel: .init( + showURL: true, + showToolbar: true, + toolbarPosition: .top, + showNavigationButtons: true, + leftToRight: false, + closeButtonText: "Close" + ), + callbackHandler: .init( + onDelegateURL: { _ in }, + onDelegateAlertController: { _ in }, + onBrowserPageLoad: {}, + onBrowserClosed: { _ in closed = true }, + onBrowserPageNavigationCompleted: { _ in } + ) + ) + let view = OSIABWebView(model) + let vStack = try view.inspect().vStack() + let hStack = try vStack.hStack(0) + // Find the button with label "Close" + var found = false + for idx in 0..<hStack.count { + if let label = try? hStack.button(idx).labelView().text().string(), label == "Close" { + try hStack.button(idx).tap() + found = true + break + } + } + XCTAssertTrue(found, "Close button not found") + XCTAssertTrue(closed) + } + + func testLoadURLCalledOnAppear() throws { + class TestModel: OSIABWebViewModel { + var loadURLCalled = false + override func loadURL() { + super.loadURL() + loadURLCalled = true + } + } + let model = TestModel( + url: URL(string: "https://test.com")!, + webViewConfiguration: .init(), + uiModel: .init( + showURL: true, + showToolbar: true, + toolbarPosition: .top, + showNavigationButtons: true, + leftToRight: false, + closeButtonText: "Close" + ), + callbackHandler: .init( + onDelegateURL: { _ in }, + onDelegateAlertController: { _ in }, + onBrowserPageLoad: {}, + onBrowserClosed: { _ in }, + onBrowserPageNavigationCompleted: { _ in } + ) + ) + let view = OSIABWebView(model) + let vStack = try view.inspect().vStack() + _ = try vStack.callOnAppear() + XCTAssertTrue(model.loadURLCalled) + } +}