In modern mobile development, testing on a fragmented landscape of devices and OS versions is the primary bottleneck for Continuous Delivery. This project provides a production-ready, cloud-first automation solution designed to eliminate that bottleneck.
By combining the readability of Cucumber BDD with the massive scale of the BrowserStack Device Cloud, this framework allows teams to execute high-concurrency test suites on hundreds of real iOS and Android devices simultaneously.
💡 Core Value Proposition Zero Infrastructure Overhead: Moves away from flaky local emulators to stable, real-world cloud hardware.
Business-Readable Testing: Uses Gherkin syntax so Product Owners and QA can collaborate on test scenarios.
High-Speed Feedback: Built with a strict Thread-Local architecture, enabling massive parallel execution to reduce regression time from hours to minutes.
Shift-Left Ready: Native integration with GitHub Actions ensures every pull request is validated on real hardware before merge.
Current Rating: 9.0/10
✅ ThreadLocal driver isolation for parallel execution
✅ BrowserStack SDK integration with YAML-based configuration
✅ Page Object Model (POM) with platform-specific locators
✅ BDD implementation using Cucumber & Gherkin
✅ Automatic test retry mechanism with RetryAnalyzer
✅ Extent Reports with screenshot capture
✅ Platform-agnostic design (Android/iOS support)
✅ CI/CD ready with BrowserStack pipeline integration
- Default TestNG suite:
testngSuite.xml - Android (tag filter required):
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=android \
-Dbrowserstack.config=browserstack-android.yml \
-Dcucumber.filter.tags="@androidOnly"- iOS (tag filter required):
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=ios \
-Dbrowserstack.config=browserstack-ios.yml \
-Dcucumber.filter.tags="@iosOnly"- Never run without the platform-specific
-Dcucumber.filter.tags; mixing Android/iOS steps in one run will fail. ├── main/java/ │ └── com/automation/framework/
- Credentials via environment variables (
BROWSERSTACK_USERNAME,BROWSERSTACK_ACCESS_KEY) or YAML (userName,accessKey). - Select platform via
-Dplatform=android|ios(default android). BrowserStack SDK reads platforms list from YAML. - YAML lives at repo root; SDK injects capabilities directly. No config.properties files.
- Waits: Configure via system property
-DimplicitWait=10or rely on BasePage defaults (10 seconds). Do NOT includeframeworkOptionsin YAML (unsupported by BrowserStack SDK). browserstack.localis not used.
├── main/java/
│ └── com/automation/framework/
│ ├── pages/
│ │ ├── BasePage.java # Base class with waits/helpers
│ │ ├── HomeScreen.java # Home screen POM
│ │ └── LoginScreen.java # Login screen POM
│ ├── driver/
│ │ ├── DriverFactory.java # Creates AppiumDriver instances
│ │ └── DriverManager.java # ThreadLocal driver isolation
│ ├── config/
│ │ └── ConfigManager.java # YAML config loader
│ └── utils/
│ ├── ElementActions.java # Common element interactions
│ └── WaitUtils.java # Explicit waits
├── test/java/
│ ├── runner/
│ │ └── TestNgRunner.java # Cucumber + TestNG runner
│ ├── stepdefinitions/
│ │ └── WdioLoginSteps.java # Step implementations
│ ├── listeners/
│ │ ├── ExtentReportListener.java # Extent Reports integration
│ │ └── ExtentReportManager.java # ThreadLocal report isolation
│ └── hooks/
│ └── Hooks.java # @Before/@After scenario hooks
├── test/resources/
│ └── features/
│ ├── BStackSample.feature # BrowserStack test scenarios
│ └── wdioLogin.feature # WDIO login test scenarios
├── pom.xml # Maven dependencies & configuration
├── testngSuite.xml # TestNG suite (parallel execution)
├── browserstack-android.yml # BrowserStack Android config
├── browserstack-ios.yml # BrowserStack iOS config
├── browserstack-android-ci.yml # BrowserStack Android CI config
└── browserstack-ios-ci.yml # BrowserStack iOS CI config
Note: Parallel execution is configured in testngSuite.xml with parallel="methods" and thread-count="N". All test commands above will automatically use this parallel configuration.
The framework uses a layered isolation model combining shared singletons for configuration and reporting with thread-local storage for driver and page object isolation. This enables safe parallel test execution without resource contention.
Key Components:
- Shared Layer: ConfigManager (YAML config), ExtentReports (single report file)
- Thread-Local Layer: AppiumDriver, PageObjectManager, ExtentTest instances
- Cleanup: @After hook with try-finally ensures guaranteed resource cleanup
Each thread gets its own isolated AppiumDriver instance via ThreadLocal:
// DriverManager.java - Thread-safe driver management
public class DriverManager {
// Each thread has its own driver instance - ZERO contention
private static final ThreadLocal<AppiumDriver> driverThreadLocal = new ThreadLocal<>();
public static AppiumDriver getDriver() {
return driverThreadLocal.get(); // Returns THIS thread's driver only
}
public static void setDriver(AppiumDriver driver) {
driverThreadLocal.set(driver); // Sets driver for THIS thread only
}
public static void quitDriver() {
AppiumDriver driver = driverThreadLocal.get();
if (driver != null) {
try {
driver.quit(); // Quit THIS thread's driver
} finally {
driverThreadLocal.remove(); // CRITICAL: Prevent memory leak
}
}
}
}Why ThreadLocal?
- No synchronization overhead (each thread has its own copy)
- Zero contention between parallel tests
- Memory is automatically cleaned up when
remove()is called
Each thread creates a driver that connects to BrowserStack cloud. The SDK handles device allocation:
// DriverFactory.java - BrowserStack cloud connection
public class DriverFactory {
private static AppiumDriver createAndroidDriver() throws MalformedURLException {
UiAutomator2Options options = new UiAutomator2Options();
// Minimal options - BrowserStack SDK injects capabilities from YAML
logger.info("Creating Android driver for BrowserStack cloud");
// Each thread gets its own BrowserStack session
return new AndroidDriver(URI.create(getBrowserStackHubUrl()).toURL(), options);
}
private static AppiumDriver createIOSDriver() throws MalformedURLException {
XCUITestOptions options = new XCUITestOptions();
// Minimal options - BrowserStack SDK injects capabilities from YAML
logger.info("Creating iOS driver for BrowserStack cloud");
// Each thread gets its own BrowserStack session
return new IOSDriver(URI.create(getBrowserStackHubUrl()).toURL(), options);
}
}BrowserStack Cloud Benefits:
- No port management needed (cloud handles isolation)
- No local Appium server setup
- Automatic device allocation from platform list in YAML
- Real devices with real network conditions
Each thread gets its own set of page objects, preventing cross-contamination:
// PageObjectManager.java - Thread-isolated page objects
public class PageObjectManager {
// Each thread gets its own PageObjectManager with its own page objects
private static final ThreadLocal<PageObjectManager> instance =
ThreadLocal.withInitial(PageObjectManager::new);
// Lazy-initialized page objects (created once per thread, reused)
private HomeScreen homeScreen;
private LoginScreen loginScreen;
public static PageObjectManager getInstance() {
return instance.get(); // Returns THIS thread's manager
}
public HomeScreen getHomeScreen() {
if (homeScreen == null) {
homeScreen = new HomeScreen(); // Lazy init for THIS thread
}
return homeScreen;
}
public LoginScreen getLoginScreen() {
if (loginScreen == null) {
loginScreen = new LoginScreen(); // Lazy init for THIS thread
}
return loginScreen;
}
public static void reset() {
instance.remove(); // Clears THIS thread's manager + all page objects
}
}Step Definitions use composition (not inheritance):
// WdioLoginSteps.java - Clean composition pattern
public class WdioLoginSteps {
// Method references avoid creating instances upfront
private HomeScreen homeScreen() {
return PageObjectManager.getInstance().getHomeScreen();
}
private LoginScreen loginScreen() {
return PageObjectManager.getInstance().getLoginScreen();
}
@Given("I navigate to the Login screen")
public void iNavigateToTheLoginScreen() {
homeScreen().navigateToLogin(); // Gets THIS thread's HomeScreen
}
}Configuration is loaded once and never modified (immutable after init):
// ConfigManager.java - Double-checked locking singleton
public class ConfigManager {
private static final Properties properties = new Properties();
private static boolean initialized = false;
public static synchronized void init() {
if (initialized) return; // Fast-path: already initialized
// Load properties (happens only ONCE across all threads)
loadProperties("config.properties", properties);
loadProperties("config-" + env + ".properties", properties);
initialized = true;
}
public static String get(String key) {
init(); // Ensure initialized (no-op if already done)
// Safe to read from multiple threads - Properties is thread-safe for reads
return System.getProperty(key, properties.getProperty(key));
}
}Why safe?
synchronized init()ensures one-time initialization- After init, properties are read-only (immutable)
java.util.Propertiesis thread-safe for concurrent reads
Single report file with thread-local test instances:
// ExtentReportManager.java - Shared report, isolated tests
public class ExtentReportManager {
private static ExtentReports extentReports; // SHARED across threads
private static final ThreadLocal<ExtentTest> extentTest = new ThreadLocal<>(); // ISOLATED
public static synchronized void initReports() {
if (extentReports == null) {
// One-time initialization (synchronized)
extentReports = new ExtentReports();
extentReports.attachReporter(new ExtentSparkReporter(path));
}
}
public static ExtentTest createTest(String testName) {
initReports();
// Thread-safe: createTest is synchronized internally by ExtentReports
ExtentTest test = extentReports.createTest(testName);
extentTest.set(test); // Store in THIS thread's local
return test;
}
public static void logPass(String message) {
ExtentTest test = extentTest.get(); // Get THIS thread's test
if (test != null) {
test.pass(message); // Log to THIS thread's test section
}
}
public static synchronized void flushReports() {
if (extentReports != null) {
extentReports.flush(); // Write all results to file (once at end)
}
}
}Architecture:
ExtentReports (Singleton)
├── Thread-1: ExtentTest "Login Scenario"
│ ├── Step: "Navigate to login" ✅
│ └── Step: "Enter credentials" ✅
├── Thread-2: ExtentTest "Forms Scenario"
│ ├── Step: "Open forms" ✅
│ └── Step: "Submit form" ❌
└── Thread-N: ExtentTest "Swipe Scenario"
└── ...
Critical for preventing resource leaks in parallel execution:
// Hooks.java - Guaranteed cleanup pattern
public class Hooks {
@Before
public void launchApplication(Scenario scenario) {
ExtentReportManager.createTest(scenario.getName());
DriverFactory.createDriver(); // Creates driver, stores in ThreadLocal
}
@After
public void executeAfterScenario(Scenario scenario) {
try {
// Handle screenshots, logging, etc.
if (scenario.isFailed() && DriverManager.hasDriver()) {
byte[] screenshot = DriverManager.getDriver().getScreenshotAs(OutputType.BYTES);
scenario.attach(screenshot, "image/png", "failure");
}
} catch (Exception e) {
logger.warn("Screenshot failed: {}", e.getMessage());
} finally {
// ALWAYS executes - even if screenshot fails!
PageObjectManager.reset(); // Clear page objects
DriverManager.quitDriver(); // Quit driver + clear ThreadLocal
}
}
}Why try-finally?
- Ensures cleanup happens even if screenshot capture fails
- Prevents driver/memory leaks that could crash parallel runs
- Maintains isolation between test scenarios
Each test thread has isolated copies of driver, page objects, and test reporting:
| Component | Isolation Level | Storage | Thread-Safe |
|---|---|---|---|
| AppiumDriver | Per thread | ThreadLocal | ✅ ZERO contention |
| PageObjectManager | Per thread | ThreadLocal | ✅ No shared state |
| ExtentTest | Per thread | ThreadLocal | ✅ Isolated test nodes |
| ConfigManager | Shared (read-only) | Static singleton | ✅ Synchronized init only |
| ExtentReports | Shared (write-safe) | Static singleton | ✅ Internally synchronized |
Tests execute in parallel via TestNG with isolated threads per scenario:
# Standard parallel execution (configured in testngSuite.xml)
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=android \
-Dbrowserstack.config=browserstack-android.yml \
-Dcucumber.filter.tags="@androidOnly"testngSuite.xml configuration:
<suite name="MobileAutomation-BDD-Suite" parallel="methods" thread-count="4">
<listeners>
<listener class-name="listeners.ExtentReportListener"/>
<listener class-name="listeners.RetryTransformer"/>
</listeners>
<test name="Mobile-BDD-Tests">
<classes>
<class name="runner.TestNgRunner"/>
</classes>
</test>
</suite>Parallelization Strategy:
parallel="methods": Each test method runs in its own threadthread-count="4": Maximum 4 concurrent threads- Thread-local storage: Each thread maintains isolated driver, page objects, and test reporting
| Component | Pattern | Thread-Safe | Notes |
|---|---|---|---|
| DriverManager | ThreadLocal | ✅ | Each thread has isolated driver; BrowserStack cloud handles session isolation |
| PageObjectManager | ThreadLocal | ✅ | Each thread has isolated page objects; cleaned up via @After hook |
| ExtentReportManager | ThreadLocal + Synchronized | ✅ | Shared report file with isolated test nodes per thread |
| ConfigManager | Synchronized init | ✅ | One-time YAML load; immutable after initialization |
| DriverFactory | BrowserStack SDK | ✅ | SDK manages device allocation and capabilities injection |
| BasePage | Stateless | ✅ | Accesses thread's driver via DriverManager.getDriver() |
| Hooks | try-finally | ✅ | Guaranteed cleanup prevents memory leaks in parallel execution |
| Pattern | Implementation | Benefit |
|---|---|---|
| Page Object Model | BasePage → HomeScreen/LoginScreen → Step Definitions |
Separation of concerns, maintainable |
| Factory Pattern | DriverFactory with atomic port allocation |
Platform-agnostic, thread-safe driver creation |
| Singleton Pattern | ConfigManager with synchronized init |
Centralized multi-env configuration |
| Thread-Local | DriverManager, PageObjectManager, ExtentTest |
Complete thread isolation for parallel execution |
| Composition over Inheritance | Step definitions use PageObjectManager.getInstance() |
Flexible, avoids diamond problem |
| SDK Integration | BrowserStack Java SDK manages capabilities, devices | Eliminates manual configuration overhead |
| Lazy Initialization | PageObjectManager.getHomeScreen() |
Memory efficient, on-demand creation |
| Template Method | BasePage defines common actions |
DRY, consistent element interactions |
| Strategy Pattern | Element-based assertions with fallbacks | Reliable mobile element detection |
- Java 21+
- Maven 3.8+
- BrowserStack account with active credentials
# Clone the repository
git clone <repository-url>
cd MobileAutomation_BDD_Framework
# Install dependencies
mvn clean install -DskipTestsAll tests run on BrowserStack cloud infrastructure using the SDK agent. See the BrowserStack (SDK-Only) section for detailed instructions.
# Set environment variables before running tests
export BROWSERSTACK_USERNAME=your-username
export BROWSERSTACK_ACCESS_KEY=your-access-key
# Run tests (DriverFactory falls back to env vars if YAML not found)
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=android \
-Dbrowserstack.config=browserstack-android.yml \
-Dcucumber.filter.tags="@androidOnly"- Credentials stored as repository secrets:
BROWSERSTACK_USERNAME,BROWSERSTACK_ACCESS_KEY - GitHub Actions injects via environment variables
- YAML files
browserstack-*.ymlare in.gitignoreto prevent accidental commits
- ❌ Exposes secrets in version control
- ❌ Requires credential rotation for every developer leaving team
- ❌ Violates compliance (SOC 2, ISO 27001)
- ✅ Use environment variables instead (one secret per environment)
pom.xmlsets default:browserstack-android.yml- Running:
mvn clean test -Dplatform=android→ loadsbrowserstack-android.yml
- Override with:
mvn clean test -Dplatform=ios -Dbrowserstack.config=browserstack-ios-ci.yml - Runs: loads
browserstack-ios-ci.ymlinstead of default
| File | Purpose | Location | Credentials |
|---|---|---|---|
browserstack-android.yml |
Android development/local tests | Repo root | ${BROWSERSTACK_USERNAME}, ${BROWSERSTACK_ACCESS_KEY} |
browserstack-ios.yml |
iOS development/local tests | Repo root | ${BROWSERSTACK_USERNAME}, ${BROWSERSTACK_ACCESS_KEY} |
browserstack-android-ci.yml |
Android CI/CD pipeline tests | Repo root | ${BROWSERSTACK_USERNAME}, ${BROWSERSTACK_ACCESS_KEY} (injected by GitHub Actions) |
browserstack-ios-ci.yml |
iOS CI/CD pipeline tests | Repo root | ${BROWSERSTACK_USERNAME}, ${BROWSERSTACK_ACCESS_KEY} (injected by GitHub Actions) |
How ConfigManager Loads Files:
- Looks for file specified in
-Dbrowserstack.configparameter - Falls back to pom.xml default if not specified
- Reads from repo root,
src/test/resources/, ortarget/test-classes/(in that order) - All environment variable placeholders (
${BROWSERSTACK_USERNAME}) are resolved at runtime
- SDK-Driven: BrowserStack Java SDK agent manages all capabilities, app upload, and session lifecycle.
- No hubUrl in YAML: SDK sets hub endpoint internally via agent. Remove
hubUrlfrom config. - Credentials: Load via environment variables (
BROWSERSTACK_USERNAME,BROWSERSTACK_ACCESS_KEY). Never hardcode for CI/CD. - App Reference: Use
custom_id(string identifier) orbs://<id>(uploaded app ID) to avoid stale references.
- Prerequisites: BrowserStack account; Java 21+; Maven 3.8+.
- Config: browserstack-android.yml with:
app: custom_id:my-android-app(recommended) ORapp: bs://uploaded-app-id- NO
hubUrl(SDK handles endpoint) platformswith device names and OS versions
- Credentials: Provide via environment:
export BROWSERSTACK_USERNAME=<your-username>
export BROWSERSTACK_ACCESS_KEY=<your-access-key>- Run:
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=android \
-Dbrowserstack.config=browserstack-android.yml \
-Dcucumber.filter.tags="@androidOnly"- Verify: BrowserStack dashboard shows sessions; logs confirm app ID and device allocation.
- Prerequisites: BrowserStack account; Java 21+; Maven 3.8+.
- Config: browserstack-ios.yml with:
app: custom_id:my-ios-app(recommended) ORapp: bs://uploaded-app-id- NO
hubUrl(SDK handles endpoint) platformswith device names and OS versions
- Credentials: Provide via environment (same as Android above).
- Run:
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=ios \
-Dbrowserstack.config=browserstack-ios.yml \
-Dcucumber.filter.tags="@iosOnly"- Verify: BrowserStack dashboard shows sessions; logs confirm app ID and device allocation.
All platform-specific commands MUST include the -Dcucumber.filter.tags parameter to isolate tests by platform. This prevents cross-platform test contamination and ensures correct locator evaluation.
- Locator Compatibility: Android and iOS interpret XPath, CSS, and ID locators differently. Running Android-only tests on iOS (or vice versa) causes
ClassCastExceptionand element interaction failures. - Driver Response Marshalling: The BrowserStack Java SDK formats element responses differently per platform. Platform-aware tests account for these differences.
- Step Isolation: Steps marked with
@androidOnlyuse Android-specific navigation; steps marked with@iosOnlyuse iOS-specific gestures.
@androidOnly: Tests/steps that execute ONLY on Android@iosOnly: Tests/steps that execute ONLY on iOS@BStackSample: Shared scenarios (not platform-specific; optional)
# Android: Run only @androidOnly scenarios
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=android \
-Dbrowserstack.config=browserstack-android.yml \
-Dcucumber.filter.tags="@androidOnly"
# iOS: Run only @iosOnly scenarios
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=ios \
-Dbrowserstack.config=browserstack-ios.yml \
-Dcucumber.filter.tags="@iosOnly"
# Multi-tag (AND logic): Run scenarios tagged with both @androidOnly AND @smoke
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=android \
-Dbrowserstack.config=browserstack-android.yml \
-Dcucumber.filter.tags="@androidOnly and @smoke"
# Exclusive execution (NOT logic): Run all scenarios EXCEPT @androidOnly (useful for iOS)
mvn clean test \
-DsuiteXmlFile=testngSuite.xml \
-Dplatform=ios \
-Dbrowserstack.config=browserstack-ios.yml \
-Dcucumber.filter.tags="not @androidOnly"- ❌ Missing tag filter:
mvn clean test -Dplatform=ios -Dbrowserstack.config=browserstack-ios.yml→ Runs ALL scenarios including@androidOnly, causing failures on iOS. - ❌ Wrong tag:
mvn clean test -Dplatform=ios -Dbrowserstack.config=browserstack-ios.yml -Dcucumber.filter.tags="@androidOnly"→ Runs Android tests on iOS; element interactions fail. - ❌ Typo in tag:
mvn clean test -Dplatform=android -Dbrowserstack.config=browserstack-android.yml -Dcucumber.filter.tags="@androidonly"(lowercase) → Tag doesn't match (case-sensitive); no tests run.
@androidOnly
Scenario: Login with valid credentials (Android-specific)
Given User opens the wdiodemoapp on Android
When User enters username and taps login
Then Login success is displayed
@iosOnly
Scenario: Verify Alert Functionality (iOS-specific)
Given User opens the BStackSampleApp on iOS
When User taps the alert trigger button
Then Alert is displayed
And User accepts the alert
@BStackSample
Scenario: Shared flow (both platforms)
Given User opens the app
When User performs a generic action
Then Generic success is displayedAlways validate your feature file tags match your -Dcucumber.filter.tags parameter before running tests.
The framework includes comprehensive GitHub Actions workflows for automated testing in CI/CD pipelines.
1. BrowserStack SDK Tests (browserstack-sdk.yml)
Automated BrowserStack testing using SDK agent for both Android and iOS.
Triggers:
- Push to
main,develop,appiumMobilebranches - Pull requests to
main,develop - Manual dispatch with platform selection
Jobs:
- ✅ Build & Validate
- 🤖 Android SDK Tests (parallel across 3 devices)
- 🍎 iOS SDK Tests (parallel across 3 devices)
- 📊 Test Summary & Report Generation
Manual Trigger:
# Via GitHub UI: Actions tab → BrowserStack SDK Tests → Run workflow
# Select platform: android | ios | both
# Optional: Custom Cucumber tags (defaults: @androidOnly, @iosOnly)Required Secrets:
BROWSERSTACK_USERNAME=<your-browserstack-username>
BROWSERSTACK_ACCESS_KEY=<your-browserstack-access-key>- Go to Settings → Secrets and variables → Actions
- Add repository secrets:
BROWSERSTACK_USERNAME: Your BrowserStack usernameBROWSERSTACK_ACCESS_KEY: Your BrowserStack access key
Workflows are automatically enabled after pushing to the repository. Verify:
git push origin appiumMobile- Navigate to Actions tab in GitHub
- View real-time logs for each job
- Download artifacts (test reports, logs) after completion
# Triggered automatically on push/PR
# Or manually with platform selection
jobs:
test-android-sdk:
runs-on: ubuntu-latest
steps:
- name: Run Android SDK Tests
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
run: |
mvn clean test \
-Pbrowserstack \
-Dplatform=android \
-Denv=browserstack \
-Dcucumber.filter.tags="@androidOnly"# Manual workflow dispatch with custom tags
workflow_dispatch:
inputs:
tags:
description: 'Cucumber tags filter'
default: '@Smoke'
# Usage in job:
-Dcucumber.filter.tags="${{ github.event.inputs.tags }}"# Both platforms run in parallel (independent jobs)
jobs:
test-android-sdk:
runs-on: ubuntu-latest
# Android tests run independently
test-ios-sdk:
runs-on: ubuntu-latest
# iOS tests run independently (no dependency on Android)All workflows upload test reports as artifacts:
Artifact Types:
- 📊 Extent Reports: HTML reports with screenshots and step details
- 📋 Surefire Reports: XML test results for GitHub UI integration
- 📝 Cucumber Reports: JSON reports for trend analysis
- 🔍 Logs: Debug logs (uploaded only on failure)
Accessing Artifacts:
- Go to Actions → Select workflow run
- Scroll to Artifacts section at the bottom
- Download zip files (e.g.,
android-sdk-reports.zip) - Extract and open
extent-reports/ExtentReport.html
Artifact Retention:
- Test reports: 30 days
- Debug logs: 7 days
-
Branch Protection Rules:
# Require BrowserStack SDK tests to pass before merging Settings → Branches → main → Require status checks: - Android SDK Test Results ✅ - iOS SDK Test Results ✅
-
Scheduled Runs:
# Add to workflow for nightly regression on: schedule: - cron: '0 2 * * *' # 2 AM UTC daily
-
Slack Notifications:
# Add step to notify on failure - name: Notify Slack if: failure() uses: slackapi/slack-github-action@v1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK }}
-
Dependency Caching:
# Already configured in workflows - uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
-
Matrix Strategy for Multiple Environments:
strategy: matrix: environment: [staging, production] platform: [android, ios] # Runs 4 jobs: staging-android, staging-ios, prod-android, prod-ios
| Issue | Solution |
|---|---|
| BrowserStack credentials error | Verify secrets are added: BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY |
| No tests executed | Check Cucumber tag filter matches feature file tags (case-sensitive) |
| Maven dependency timeout | Enable dependency caching; increase MAVEN_OPTS memory |
| iOS Simulator boot failure | Use macos-13 runner (has Xcode 14); macos-latest may vary |
| Parallel execution conflicts | Ensure proper port management in DriverFactory (already configured) |
| Artifact upload size limit | Compress large reports; reduce screenshot size in Extent config |
Add status badges to README:
[](https://github.com/<owner>/<repo>/actions/workflows/browserstack-sdk.yml)