diff --git a/.github/workflows/sdk-sample-test.yml b/.github/workflows/sdk-sample-test.yml new file mode 100644 index 0000000..044c19e --- /dev/null +++ b/.github/workflows/sdk-sample-test.yml @@ -0,0 +1,72 @@ +# Runs the BrowserStack SDK sample against a given commit and reports a status check. +# Trigger: Actions tab -> "Vanilla Java SDK sample test" -> Run workflow -> paste the PR's full commit SHA. +# Requires repo secrets: BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY. +name: Vanilla Java 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: vanilla-java JDK ${{ matrix.java }} sample + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + defaults: + run: + working-directory: . + 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: vanilla-java 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: Run sample test + run: | + mvn compile + mvn compile exec:exec + - name: Mark status check completed + if: always() + uses: actions/github-script@v7 + env: + conclusion: ${{ job.status }} + job_name: vanilla-java 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..7265af6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Maven +target/ + +# IDE +.idea/ +*.iml +.vscode/ +.project +.classpath +.settings/ + +# OS +.DS_Store + +# BrowserStack / logs +local.log +logs/ +log/ + +# Credentials — never commit +.env diff --git a/README.md b/README.md index 4521bcd..fd353e5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ -# vanilla-java-browserstack -We require the following new public repositories under the browserstack GitHub organization to host customer-facing sample projects for the BrowserStack SDK. +# Vanilla Java (Selenium) with BrowserStack + +Run Selenium WebDriver tests on [BrowserStack Automate](https://automate.browserstack.com/) +using the **BrowserStack Java SDK** — no test framework required. The SDK attaches as a +`-javaagent:`, reads `browserstack.yml`, and runs your tests across the configured platform +matrix with zero code changes for credentials, platforms, Test Observability, and Local. + +This sample contains exactly two tests: + +- **Sample test** (`BStackDemoTest`) — an add-to-cart flow on [bstackdemo.com](https://bstackdemo.com/). +- **Local test** (`BStackLocalTest`) — verifies the BrowserStack Local tunnel by opening + `http://bs-local.com:45454` and asserting the page title. + +## Prerequisites + +- A [BrowserStack account](https://www.browserstack.com/users/sign_up) (username + access key). +- **JDK 11+** — Selenium 4.23 requires Java 11 (`maven.compiler.source/target = 11`). +- **Maven 3.6+**. + +## Setup + +1. Clone this repository and change into it: + ```bash + git clone https://github.com/browserstack/vanilla-java-browserstack.git + cd vanilla-java-browserstack + ``` +2. Configure your BrowserStack credentials. Either edit `userName` / `accessKey` in + `browserstack.yml`, or export them as environment variables (env vars take precedence): + ```bash + export BROWSERSTACK_USERNAME="YOUR_USERNAME" + export BROWSERSTACK_ACCESS_KEY="YOUR_ACCESS_KEY" + ``` +3. Resolve dependencies (downloads Selenium and the BrowserStack Java SDK): + ```bash + mvn clean compile + ``` + +The `-javaagent:` jar is wired automatically: the `maven-dependency-plugin` resolves the +SDK jar path into the `${com.browserstack:browserstack-java-sdk:jar}` property, and the +`exec-maven-plugin` passes it as `-javaagent:` when launching each test class. + +## Run Sample Test + +Runs the bstackdemo.com add-to-cart flow across the platforms in `browserstack.yml`: + +```bash +mvn test +``` + +Or, equivalently, via the exec goal: + +```bash +mvn compile exec:exec +``` + +## Run Local Test + +`browserstackLocal: true` in `browserstack.yml` tells the SDK to start a BrowserStack Local +tunnel before the session, so the remote browser can reach `http://bs-local.com:45454`: + +```bash +mvn compile exec:exec -P sample-local +``` + +To run both tests concurrently: + +```bash +mvn compile exec:exec -P run-parallel +``` + +## Notes / Dashboard + +- View runs, video, logs, and network traffic at + [automate.browserstack.com](https://automate.browserstack.com/). +- With `testObservability: true`, the same build also appears in + [Test Observability](https://observability.browserstack.com/). +- Platforms, parallelism, Local, Observability, and debugging flags are all controlled from + `browserstack.yml` — no code changes needed to add browsers or devices. +- Vanilla Java has no test runner, so `browserstack.yml` intentionally omits the `framework:` + field. The `-javaagent:` still instruments the Selenium `RemoteWebDriver` so sessions are + created and reported. Add `framework:` only when adopting a supported test framework + (TestNG / JUnit 4 / JUnit 5 / Cucumber). diff --git a/browserstack.yml b/browserstack.yml new file mode 100644 index 0000000..5cb43c9 --- /dev/null +++ b/browserstack.yml @@ -0,0 +1,59 @@ +# ============================= +# 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: browserstack build +buildIdentifier: '#${BUILD_NUMBER}' +# NOTE: no `framework:` field for vanilla Java — there is no test runner for the SDK to +# instrument. The `-javaagent:` jar still instruments the Selenium RemoteWebDriver +# constructor, so sessions are created and reported. Set `framework:` only when using a +# supported test framework (testng / junit4 / junit5 / cucumber). + +# ======================================= +# Platforms (Browsers / Devices to test) +# ======================================= +platforms: + - os: OS X + osVersion: Big Sur + browserName: Chrome + browserVersion: latest + - os: Windows + osVersion: 10 + browserName: Edge + browserVersion: latest + - deviceName: Samsung Galaxy S22 Ultra + browserName: chrome + osVersion: 12.0 + +# ======================= +# Parallels per Platform +# ======================= +parallelsPerPlatform: 1 + +# ========================================== +# BrowserStack Automate + Local +# ========================================== +browserstackAutomation: true +browserstackLocal: true + +source: java:sample-sdk:v1.0 + +# ====================== +# Test Observability +# ====================== +testObservability: true + +# =================== +# Debugging features +# =================== +debug: false +networkLogs: false +consoleLogs: errors diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5e254c0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,150 @@ + + 4.0.0 + + com.browserstack + vanilla-java-browserstack + 1.0-SNAPSHOT + jar + + vanilla-java-browserstack + https://github.com/browserstack/vanilla-java-browserstack + + + UTF-8 + 11 + 11 + 4.23.0 + + + + + org.seleniumhq.selenium + selenium-java + ${selenium.version} + + + com.browserstack + browserstack-java-sdk + LATEST + compile + + + io.github.bonigarcia + webdrivermanager + 5.9.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + maven-dependency-plugin + + + getClasspathFilenames + + properties + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + test + + exec + + + + + java + + -javaagent:${com.browserstack:browserstack-java-sdk:jar} + -cp + + com.browserstack.tests.BStackDemoTest + + + + + + + + + + sample-local + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + + exec + + + + + java + + -javaagent:${com.browserstack:browserstack-java-sdk:jar} + -cp + + com.browserstack.tests.BStackLocalTest + + + + + + + + + + run-parallel + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + + exec + + + + + java + + -javaagent:${com.browserstack:browserstack-java-sdk:jar} + -cp + + com.browserstack.runner.ParallelTestRunner + + + + + + + + diff --git a/src/main/java/com/browserstack/driver/DriverFactory.java b/src/main/java/com/browserstack/driver/DriverFactory.java new file mode 100644 index 0000000..bdebaa3 --- /dev/null +++ b/src/main/java/com/browserstack/driver/DriverFactory.java @@ -0,0 +1,32 @@ +package com.browserstack.driver; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.RemoteWebDriver; + +/** + * Creates a RemoteWebDriver pointed at the BrowserStack hub. + * + * No browser/OS capabilities are set in code: the BrowserStack Java SDK + * (attached via the {@code -javaagent:} jar) reads {@code browserstack.yml}, + * injects the platform matrix, starts the session, and reports the result. + * Credentials are read from the BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY + * environment variables (or from browserstack.yml). + */ +public class DriverFactory { + + private static final String HUB_URL = "https://hub.browserstack.com/wd/hub"; + + public static WebDriver createDriver() { + try { + // Empty capabilities — the SDK injects platform + credentials from browserstack.yml. + DesiredCapabilities capabilities = new DesiredCapabilities(); + return new RemoteWebDriver(new URL(HUB_URL), capabilities); + } catch (MalformedURLException e) { + throw new RuntimeException("Invalid BrowserStack hub URL: " + HUB_URL, e); + } + } +} diff --git a/src/main/java/com/browserstack/runner/ParallelTestRunner.java b/src/main/java/com/browserstack/runner/ParallelTestRunner.java new file mode 100644 index 0000000..5aa5f88 --- /dev/null +++ b/src/main/java/com/browserstack/runner/ParallelTestRunner.java @@ -0,0 +1,30 @@ +package com.browserstack.runner; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import com.browserstack.tests.BStackDemoTest; +import com.browserstack.tests.BStackLocalTest; + +/** + * Runs both sample tests concurrently. Each Runnable creates its own session + * against the BrowserStack hub; the SDK reports each one independently. + */ +public class ParallelTestRunner { + + public static void main(String[] args) throws InterruptedException { + List tests = Arrays.asList( + new BStackDemoTest(), + new BStackLocalTest()); + + ExecutorService executor = Executors.newFixedThreadPool(tests.size()); + for (Runnable test : tests) { + executor.submit(test); + } + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.MINUTES); + } +} diff --git a/src/main/java/com/browserstack/tests/BStackDemoTest.java b/src/main/java/com/browserstack/tests/BStackDemoTest.java new file mode 100644 index 0000000..434f960 --- /dev/null +++ b/src/main/java/com/browserstack/tests/BStackDemoTest.java @@ -0,0 +1,64 @@ +package com.browserstack.tests; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import com.browserstack.driver.DriverFactory; + +/** + * Sample test — e-commerce add-to-cart flow on https://bstackdemo.com/. + * + * Vanilla Java (no test framework): the flow runs inside {@code main()}. + * The BrowserStack Java SDK ({@code -javaagent:}) instruments the RemoteWebDriver + * constructor, starts the session, and marks it passed/failed automatically. + * It also implements {@link Runnable} so it can be driven by ParallelTestRunner. + */ +public class BStackDemoTest implements Runnable { + + @Override + public void run() { + try { + main(null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) throws Exception { + WebDriver driver = DriverFactory.createDriver(); + try { + driver.get("https://bstackdemo.com/"); + + // 1. Read the first product's name from the listing. + WebElement productElement = driver.findElement(By.xpath("//*[@id=\"1\"]/p")); + String productName = productElement.getText(); + System.out.println("Product on screen: " + productName); + + // 2. Click that product's "Add to cart". + driver.findElement(By.xpath("//*[@id=\"1\"]/div[4]")).click(); + + // 3. Wait for the cart pane to render. + Thread.sleep(3000); + WebElement cartContent = driver.findElement(By.cssSelector(".float-cart__content")); + if (!cartContent.isDisplayed()) { + throw new AssertionError("Cart pane (float-cart__content) is not displayed"); + } + + // 4. Read the product name shown in the cart and assert it matches. + WebElement productInCart = driver.findElement(By.xpath("//p[@class='title']")); + String cartProductName = productInCart.getText(); + System.out.println("Product in cart: " + cartProductName); + + if (!productName.equals(cartProductName)) { + throw new AssertionError( + "Product in cart '" + cartProductName + "' does not match product on screen '" + + productName + "'"); + } + + System.out.println("Sample test passed!"); + } finally { + driver.quit(); + } + } +} diff --git a/src/main/java/com/browserstack/tests/BStackLocalTest.java b/src/main/java/com/browserstack/tests/BStackLocalTest.java new file mode 100644 index 0000000..156a329 --- /dev/null +++ b/src/main/java/com/browserstack/tests/BStackLocalTest.java @@ -0,0 +1,43 @@ +package com.browserstack.tests; + +import org.openqa.selenium.WebDriver; + +import com.browserstack.driver.DriverFactory; + +/** + * Local test — proves the BrowserStack Local tunnel is connected. + * + * With {@code browserstackLocal: true} in browserstack.yml, the SDK starts a + * Local tunnel, so the remote browser can reach http://bs-local.com:45454. + * The page served there has the title "BrowserStack Local". + */ +public class BStackLocalTest implements Runnable { + + @Override + public void run() { + try { + main(null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) throws Exception { + WebDriver driver = DriverFactory.createDriver(); + try { + driver.get("http://bs-local.com:45454/"); + + String title = driver.getTitle(); + System.out.println("Local page title: " + title); + + if (!"BrowserStack Local".equals(title)) { + throw new AssertionError( + "Title does not match 'BrowserStack Local' (got '" + title + "')"); + } + + System.out.println("Local test passed!"); + } finally { + driver.quit(); + } + } +}