diff --git a/.github/workflows/sdk-sample-test.yml b/.github/workflows/sdk-sample-test.yml new file mode 100644 index 0000000..1ee9a1f --- /dev/null +++ b/.github/workflows/sdk-sample-test.yml @@ -0,0 +1,78 @@ +# Runs the BrowserStack SDK sample against a given commit and reports a status check. +# Trigger: Actions tab -> "Gauge Appium App Automate SDK sample test" -> Run workflow -> paste the PR's full commit SHA. +# Requires repo secrets: BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY. +# NOTE (App Automate): the app under test is referenced via `app: bs://...` in browserstack.yml; +# ensure that uploaded app exists on the account whose secrets are used (re-upload + update if expired). +name: Gauge Appium App Automate SDK sample test + +on: + workflow_dispatch: + inputs: + commit_sha: + description: 'The full commit id to build' + required: true + +permissions: + contents: read # checkout + checks: write # github-script creates the status check + +jobs: + sdk-sample: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + max-parallel: 3 + matrix: + os: [ubuntu-latest] + java: ['11', '17'] + name: gauge-appium JDK ${{ matrix.java }} sample + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + defaults: + run: + working-directory: android + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.commit_sha }} + - name: Mark status check in_progress + uses: actions/github-script@v7 + env: + job_name: gauge-appium JDK ${{ matrix.java }} sample + commit_sha: ${{ github.event.inputs.commit_sha }} + with: + github-token: ${{ github.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, repo: context.repo.repo, + name: process.env.job_name, head_sha: process.env.commit_sha, + status: 'in_progress' + }).catch(e => console.log('check create failed:', e.status)); + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + - name: Install Gauge + run: | + npm install -g @getgauge/cli + gauge install java + - name: Run sample test + run: | + mvn compile + mvn test + - name: Mark status check completed + if: always() + uses: actions/github-script@v7 + env: + conclusion: ${{ job.status }} + job_name: gauge-appium JDK ${{ matrix.java }} sample + commit_sha: ${{ github.event.inputs.commit_sha }} + with: + github-token: ${{ github.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, repo: context.repo.repo, + name: process.env.job_name, head_sha: process.env.commit_sha, + status: 'completed', conclusion: process.env.conclusion + }).catch(e => console.log('check create failed:', e.status)); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9b93d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Gradle +.gradle/ +**/build/ +gradle-app.setting + +# Gauge +**/logs/ +**/log/ +**/reports/ +**/.gauge/ +local.log + +# BrowserStack SDK run artifacts +**/browserstack.err +**/bstack_build_*/ +**/gradle-m-config.json +**/dependency-tree.log + +# IDE +.idea/ +*.iml +.vscode/ +.settings/ +.project +.classpath + +# OS +.DS_Store + +# Credentials — never commit +*.env diff --git a/README.md b/README.md index c5a4c4c..49e75f8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,117 @@ -# gauge-appium-app-browserstack -We require the following new public repositories under the browserstack GitHub organization to host customer-facing sample projects for the BrowserStack SDK. +# Gauge + Appium with BrowserStack App Automate + +Run [Gauge](https://gauge.org/) + [Appium](https://appium.io/) (Java) mobile tests on real devices +in the [BrowserStack App Automate](https://app-automate.browserstack.com/) cloud, instrumented by the +[BrowserStack Java SDK](https://www.browserstack.com/docs/app-automate/appium/getting-started/java). + +The SDK reads `browserstack.yml`, uploads/points at the app under test, provisions the device, and reports +each scenario's name and status to App Automate and Test Observability — no per-test capability code. + +This sample currently ships the **`android/`** platform directory (Android, Samsung Galaxy S22 Ultra). + +## Prerequisites + +- A [BrowserStack](https://www.browserstack.com/) account (username + access key). +- **JDK 11+** (`java -version`). +- **Gradle 7+** (a wrapper can be generated with `gradle wrapper`). +- **Gauge CLI** with the Java plugin: + ```bash + brew install gauge # macOS (or see https://docs.gauge.org/getting_started/installing-gauge.html) + gauge install java + gauge install html-report + gauge install screenshot + ``` + +## Setup + +```bash +git clone +cd gauge-appium-app-browserstack/android +``` + +Configure credentials — set them as environment variables (recommended) or edit `android/browserstack.yml`: + +```bash +export BROWSERSTACK_USERNAME="YOUR_USERNAME" +export BROWSERSTACK_ACCESS_KEY="YOUR_ACCESS_KEY" +``` + +Resolve dependencies and stage the BrowserStack Java SDK agent: + +```bash +gradle clean build copyBrowserStackAgent +``` + +`copyBrowserStackAgent` stages the resolved `browserstack-java-sdk` jar at `build/agent/browserstack-java-sdk.jar`. +The `-javaagent` is attached to the **Gauge Java runner JVM** via `env/default/java.properties` +(`gauge_jvm_args`) — Gauge spawns its own runner JVM, so the agent must be wired there (not on the Gradle +build JVM) for the Appium driver to be instrumented. + +### App under test + +`android/browserstack.yml` points at a pre-uploaded `WikipediaSample.apk` +(`app: bs://92d48b416632f2b1734259565ceab61b05ad0b24`). To use your own build, upload it and replace the value: + +```bash +curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ + -F "file=@/path/to/WikipediaSample.apk" +# -> use the returned "app_url" (bs://...) as the `app:` value +``` + +## Run Sample Test + +Searches the Wikipedia sample app for "BrowserStack" and asserts results are listed. + +```bash +cd android +gradle runSampleTest +# or run the whole suite: gradle gauge +# or via the Gauge CLI: gauge run specs/bstack-specs/bstack-sample.spec +``` + +## Run Local Test + +Verifies the [BrowserStack Local](https://www.browserstack.com/local-testing/app-automate) tunnel is +connected. The SDK starts the tunnel automatically because `browserstackLocal: true` is set in +`browserstack.yml`. + +```bash +cd android +gradle runLocalTest +# or via the Gauge CLI: gauge run specs/bstack-specs/local-test.spec +``` + +## Notes / Dashboard + +- View runs, video, device logs, and network logs at **https://app-automate.browserstack.com/**. +- With `testObservability: true`, builds also appear at **https://observability.browserstack.com/**. +- The `framework: gauge` token in `browserstack.yml` lets the Java SDK report each scenario's name and + status to BrowserStack. +- The BrowserStack Gradle SDK plugin (`com.browserstack.gradle-sdk`) is declared per the BrowserStack + SDK convention; the actual Appium-driver instrumentation for this Gauge/Java flow is performed by the + `browserstack-java-sdk` `-javaagent` on the Gauge runner JVM (see Setup). + +### Known issue (BrowserStack Java SDK + Gauge App Automate) + +With the **published** `browserstack-java-sdk:1.59.7`, `framework: gauge` disables the SDK's normal +in-process flow and instead drives the run through the **Gradle Tooling API** (it re-invokes the +`gauge` task per device platform). On this version the SDK mis-resolves the Gradle installation to +`GRADLE_USER_HOME` (`~/.gradle`) and the Tooling-API connection fails: + +``` +GradleTaskExecutor - Using Gradle Installation /Users//.gradle +GradleTaskExecutor - Gradle connection failed for Platform Index: 0 +[SDK-TRA-006] ... could not find relevant classes from gauge on your class path +``` + +The SDK then ends its run inside the `-javaagent` premain and the Gauge runner JVM exits before any +test (and therefore any Appium driver / device session) is created. Only the Observability/TestHub +build is created; no App Automate device session starts. + +This reproduces with both `gauge run` and `gradle gauge`, with `gauge-java` 0.12.0 and 1.0.1, and with +or without the `framework` token. The known-working internal reference for this combo used **unpublished +local dev builds** (`browserstack-java-sdk-1.32.12` + `gradle-sdk` plugin `99.86.49`). Track the fix / +a published known-good SDK version with the BrowserStack Java SDK team before relying on this sample for +a live device run. The repo itself is complete and compiles; the blocker is in the SDK's Gauge App +Automate execution path, not in the test code. diff --git a/android/LocalSample.apk b/android/LocalSample.apk new file mode 100644 index 0000000..f31c574 Binary files /dev/null and b/android/LocalSample.apk differ diff --git a/android/WikipediaSample.apk b/android/WikipediaSample.apk new file mode 100644 index 0000000..03d19e6 Binary files /dev/null and b/android/WikipediaSample.apk differ diff --git a/android/browserstack.yml b/android/browserstack.yml new file mode 100644 index 0000000..de03c78 --- /dev/null +++ b/android/browserstack.yml @@ -0,0 +1,60 @@ +# ============================= +# Set BrowserStack Credentials +# ============================= +# Add your BrowserStack userName and accessKey here or set BROWSERSTACK_USERNAME and +# BROWSERSTACK_ACCESS_KEY as env variables. +userName: YOUR_USERNAME +accessKey: YOUR_ACCESS_KEY + +# ====================== +# BrowserStack Reporting +# ====================== +projectName: BrowserStack Samples +buildName: appauto-gauge-appium +buildIdentifier: '#${BUILD_NUMBER}' +# `framework` lets the BrowserStack Java SDK (javaagent) instrument the Gauge runner and report each +# scenario's name and status to BrowserStack. The SDK's gauge instrumentation is validated against the +# Gauge `java` runner plugin + gauge-java library 0.12.0 (see build.gradle); keep those aligned. +framework: gauge + +# ====================================== +# Public sample app committed in this repo (relative path); the SDK uploads it at run time. +# ====================================== +# WikipediaSample.apk, already uploaded to BrowserStack App Automate. +# To upload your own build, run: +# curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ +# -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ +# -F "file=@/path/to/WikipediaSample.apk" +# Public sample app committed in this repo (relative path); the SDK uploads it at run time. +app: ./WikipediaSample.apk + +# ======================================= +# Platforms (real devices to test on) +# ======================================= +platforms: + - deviceName: Samsung Galaxy S22 Ultra + osVersion: "12.0" + platformName: android + +# ======================= +# Parallels per Platform +# ======================= +parallelsPerPlatform: 1 + +# ========================================== +# BrowserStack Local (for the local test) +# ========================================== +browserstackLocal: true + +source: gauge:appium-sample-sdk:v1.0 + +# ====================== +# Test Observability +# ====================== +testObservability: true + +# =================== +# Debugging features +# =================== +debug: true +networkLogs: true diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..05af127 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,103 @@ +plugins { + id 'java' + id 'org.gauge' version '1.8.2' + // BrowserStack Gradle SDK plugin (latest). Declared per the BrowserStack SDK convention. + id 'com.browserstack.gradle-sdk' version '3.1.6' +} + +apply plugin: 'com.browserstack.gradle-sdk' + +group 'com.browserstack' +version '1.0-SNAPSHOT' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +repositories { + mavenCentral() + mavenLocal() +} + +configurations.all { + resolutionStrategy { + eachDependency { details -> + if (details.requested.group == 'com.google.guava' && details.requested.name == 'guava') { + details.useVersion '32.1.2-jre' + details.because 'Align Guava across Selenium/Appium/Gauge for Java 11+' + } + } + } +} + +dependencies { + implementation 'com.google.guava:guava:32.1.2-jre' + // gauge-java MUST match the installed Gauge Java runner plugin version. The BrowserStack Java SDK's + // gauge instrumentation is validated against gauge-java 0.12.0 (the reference pin); pair it with the + // matching Gauge `java` runner plugin (0.12.0). A newer gauge-java (1.0.1) trips SDK error SDK-TRA-006. + implementation 'com.thoughtworks.gauge:gauge-java:0.12.0' + implementation 'org.seleniumhq.selenium:selenium-java:4.13.0' + implementation 'io.appium:java-client:8.6.0' + implementation 'com.browserstack:browserstack-local-java:1.0.6' + implementation 'commons-io:commons-io:2.11.0' + testImplementation 'org.assertj:assertj-core:3.20.2' + + // BrowserStack Java SDK — provides the -javaagent that instruments the Appium driver and + // injects app + device capabilities from browserstack.yml. The agent is attached to the + // Gauge Java *runner* JVM via env/default/java.properties (gauge_jvm_args). Resolving LATEST + // from Maven Central also makes the jar available under ~/.m2 / the Gradle cache so the + // -javaagent path can point at it. See README.md. + // NOTE: Gradle does not understand Maven's "LATEST" keyword — use "latest.release". + implementation 'com.browserstack:browserstack-java-sdk:latest.release' +} + +// Convenience: copy the resolved BrowserStack Java SDK jar into build/agent so env/default/java.properties +// can reference a stable, version-independent path (build/agent/browserstack-java-sdk.jar). +task copyBrowserStackAgent(type: Copy) { + description = 'Stage the BrowserStack Java SDK jar for the -javaagent on the Gauge runner JVM.' + from { + configurations.runtimeClasspath.find { it.name ==~ /browserstack-java-sdk-.*\.jar/ } + } + into "$projectDir/build/agent" + rename { 'browserstack-java-sdk.jar' } +} + +// Helper: build a Gauge-CLI run task. +// We invoke the Gauge CLI directly (rather than the org.gauge GaugeTask, which subclasses Gradle's +// `Test` task and is skipped as NO-SOURCE because there are no JUnit/TestNG classes). The Gauge Java +// runner classpath is provided explicitly via the gauge_custom_classpath env var, sourced from the +// project's compiled test classes + full runtime dependencies. The -javaagent is wired separately on +// the Gauge runner JVM via env/default/java.properties (gauge_jvm_args). +def gaugeRunTask = { String taskName, String specPath, String desc -> + tasks.register(taskName, Exec) { + group = 'verification' + description = desc + dependsOn 'testClasses', 'copyBrowserStackAgent' + workingDir projectDir + doFirst { + def cp = sourceSets.test.runtimeClasspath.files + .collect { it.absolutePath }.join(File.pathSeparator) + environment 'gauge_custom_classpath', cp + commandLine 'gauge', 'run', '--verbose', specPath + } + } +} + +gaugeRunTask('runSampleTest', 'specs/bstack-specs/bstack-sample.spec', + 'Run the WikipediaSample sample spec on BrowserStack App Automate.') +gaugeRunTask('runLocalTest', 'specs/bstack-specs/local-test.spec', + 'Run the LocalSample local-connection spec on BrowserStack App Automate.') +gaugeRunTask('runTests', 'specs/bstack-specs', + 'Run the full Gauge spec suite on BrowserStack App Automate.') + +// Print the test runtime classpath (compiled test classes + all runtime deps) so the Gauge CLI can +// also be invoked manually: `export gauge_custom_classpath=$(cat build/gauge-classpath.txt); gauge run …` +task printGaugeClasspath { + dependsOn 'testClasses' + doLast { + def cp = sourceSets.test.runtimeClasspath.files.collect { it.absolutePath }.join(File.pathSeparator) + new File("$projectDir/build/gauge-classpath.txt").text = cp + println "WROTE_CLASSPATH" + } +} diff --git a/android/env/default/default.properties b/android/env/default/default.properties new file mode 100644 index 0000000..eeca4c4 --- /dev/null +++ b/android/env/default/default.properties @@ -0,0 +1,27 @@ +# default.properties +# Properties set here are available to the test execution as environment variables. + +# The path to the Gauge reports directory. Relative to the project directory or an absolute path. +gauge_reports_dir = reports + +# Set as false if Gauge reports should not be overwritten on each execution. +# A new time-stamped directory will be created on each execution. +overwrite_reports = true + +# Set to false to disable screenshots on failure in reports. +screenshot_on_failure = true + +# The path to the Gauge logs directory. Relative to the project directory or an absolute path. +logs_directory = logs + +# Set to true to use multithreading for parallel execution. +enable_multithreading = false + +# The path to the Gauge specifications directory. +gauge_specs_dir = specs/bstack-specs + +# The default delimiter used to read csv files. +csv_delimiter = , + +# Allows steps to be written in multiline. +allow_multiline_step = false diff --git a/android/env/default/java.properties b/android/env/default/java.properties new file mode 100644 index 0000000..586f0c6 --- /dev/null +++ b/android/env/default/java.properties @@ -0,0 +1,25 @@ +# Specify an alternate Java home if you want to use a custom version +gauge_java_home = + +# IntelliJ and Eclipse out directory will be usually autodetected +# Use the below property if you need to override the build path +gauge_custom_build_path = + +# Specify the directory where additional libs are kept. +# Multiple directories can be specified, separated with a comma (,) +gauge_additional_libs = libs/* + +# JVM arguments passed to the Gauge Java runner JVM. +# This is the load-bearing wiring for the BrowserStack Java SDK: the -javaagent must be attached +# to the *runner* JVM (Gauge spawns its own JVM separate from the Gradle build JVM, so wiring the +# agent only on the Gradle test task would NOT instrument the Appium driver). The jar is staged at +# build/agent/browserstack-java-sdk.jar by the `copyBrowserStackAgent` Gradle task. +gauge_jvm_args = -javaagent:build/agent/browserstack-java-sdk.jar + +# Specify the directory containing java files to be compiled. +# Multiple directories can be specified, separated with a comma (,) +gauge_custom_compile_dir = + +# The level at which the objects should be cleared. +# Possible values are suite, spec and scenario. Default value is scenario. +gauge_clear_state_level = scenario diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d997cfc Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b82aa23 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..0262dcb --- /dev/null +++ b/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/manifest.json b/android/manifest.json new file mode 100644 index 0000000..82ea7e3 --- /dev/null +++ b/android/manifest.json @@ -0,0 +1,7 @@ +{ + "Language": "java", + "Plugins": [ + "html-report", + "screenshot" + ] +} diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..df84ec2 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + mavenLocal() + google() + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == 'org.gauge') { + useModule('org.gauge:org.gauge.gradle.plugin:1.8.2') + } + } + } +} +rootProject.name = 'gauge-appium' diff --git a/android/specs/bstack-specs/bstack-sample.spec b/android/specs/bstack-specs/bstack-sample.spec new file mode 100644 index 0000000..77dc685 --- /dev/null +++ b/android/specs/bstack-specs/bstack-sample.spec @@ -0,0 +1,13 @@ +BrowserStack Sample (WikipediaSample.apk) +========================================= + +The following scenario opens the Wikipedia sample app, searches for "BrowserStack", +and asserts that search results are listed. + +Search Wikipedia for BrowserStack +--------------------------------- +tags: search, smoke + +* I tap on the Search Wikipedia button +* I search with keyword "BrowserStack" +* The search results should be listed diff --git a/android/specs/bstack-specs/local-test.spec b/android/specs/bstack-specs/local-test.spec new file mode 100644 index 0000000..a1b1113 --- /dev/null +++ b/android/specs/bstack-specs/local-test.spec @@ -0,0 +1,12 @@ +Verify Local test (LocalSample.apk) +=================================== + +The following scenario verifies the BrowserStack Local tunnel is connected by +launching the local sample app and asserting it reports an active connection. + +Verify Local connection +----------------------- +tags: local, smoke + +* I am on the local app +* I verify active connection on the app diff --git a/android/src/test/java/WikipediaSteps.java b/android/src/test/java/WikipediaSteps.java new file mode 100644 index 0000000..98a68d6 --- /dev/null +++ b/android/src/test/java/WikipediaSteps.java @@ -0,0 +1,112 @@ +import com.thoughtworks.gauge.AfterSpec; +import com.thoughtworks.gauge.BeforeSpec; +import com.thoughtworks.gauge.Step; +import io.appium.java_client.AppiumBy; +import io.appium.java_client.android.AndroidDriver; +import io.appium.java_client.android.options.UiAutomator2Options; +import java.net.URL; +import java.time.Duration; +import java.util.List; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Gauge step implementations for the BrowserStack App Automate (Appium, Android) sample. + * + * The driver is created with an EMPTY options object against the BrowserStack hub. + * The BrowserStack Java SDK (attached as a -javaagent to the Gauge runner JVM) injects the + * app + device capabilities from browserstack.yml at session creation time. + */ +public class WikipediaSteps { + + private static final String HUB_URL = "https://hub.browserstack.com/wd/hub"; + private static WebDriver driver; + + @BeforeSpec + public void setUp() throws Exception { + // Empty options — the BrowserStack SDK injects app/device caps from browserstack.yml. + UiAutomator2Options options = new UiAutomator2Options(); + driver = new AndroidDriver(new URL(HUB_URL), options); + } + + // ===== Sample test: WikipediaSample.apk ===== + + @Step("I tap on the Search Wikipedia button") + public void tapSearchWikipedia() { + WebElement search = new WebDriverWait(driver, Duration.ofSeconds(30)).until( + ExpectedConditions.elementToBeClickable(AppiumBy.accessibilityId("Search Wikipedia"))); + search.click(); + } + + @Step("I search with keyword ") + public void searchWithKeyword(String keyword) { + WebElement input = new WebDriverWait(driver, Duration.ofSeconds(30)).until( + ExpectedConditions.elementToBeClickable( + AppiumBy.id("org.wikipedia.alpha:id/search_src_text"))); + input.sendKeys(keyword); + } + + @Step("The search results should be listed") + public void searchResultsShouldBeListed() { + WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); + try { + wait.until(ExpectedConditions.presenceOfElementLocated( + AppiumBy.className("android.widget.TextView"))); + } catch (TimeoutException e) { + throw new AssertionError("Search results did not load within 30 seconds"); + } + List results = driver.findElements(AppiumBy.className("android.widget.TextView")); + assertThat(results) + .withFailMessage("Expected the Wikipedia search to list results, but none were found") + .isNotEmpty(); + } + + // ===== Local test: LocalSample.apk ===== + + @Step("I am on the local app") + public void iAmOnTheLocalApp() throws InterruptedException { + WebElement action = new WebDriverWait(driver, Duration.ofSeconds(30)).until( + ExpectedConditions.elementToBeClickable( + AppiumBy.id("com.example.android.basicnetworking:id/test_action"))); + action.click(); + Thread.sleep(5000); + } + + @Step("I verify active connection on the app") + public void iVerifyActiveConnection() { + new WebDriverWait(driver, Duration.ofSeconds(30)).until( + ExpectedConditions.presenceOfElementLocated( + AppiumBy.className("android.widget.TextView"))); + List textViews = driver.findElements(AppiumBy.className("android.widget.TextView")); + WebElement matched = null; + for (WebElement el : textViews) { + if (el.getText() != null && el.getText().contains("The active connection is")) { + matched = el; + break; + } + } + if (matched == null) { + throw new AssertionError( + "Could not find any TextView containing 'The active connection is' — " + + "is the BrowserStack Local tunnel connected?"); + } + String text = matched.getText(); + System.out.println("Local app reported: " + text); + assertThat(text) + .withFailMessage("Expected the local app to report it is up and running, got: %s", text) + .contains("Up and running"); + } + + @AfterSpec + public void tearDown() { + if (driver != null) { + driver.quit(); + driver = null; + } + } +}