← → to navigate

GienTech · Test Automation Enablement

Onboarding to the pCloudy Device Farm

What every team needs before their Playwright browser tests or Java Appium mobile tests will run on real cloud devices.

Browser Cloud · Playwright Mobile Farm · Java Appium
Two products, one account

Browser Cloud runs desktop browsers. The Mobile Device Farm runs real Android and iOS phones. One login and one API key covers both, but each has its own endpoint and setup. Slides are marked 🖥 for browser and 📱 for mobile.

Orientation

What actually changes when you go remote

Your test code does not change. Only where the browser or device lives, and how the session is requested.

🖥 Browser — Playwright
// local
chromium.launch()

// pCloudy Browser Cloud
chromium.connect(
  `wss://browser.device.pcloudy.com/playwright
   ?capabilities=${encoded}`)
📱 Mobile — Appium
// local
new AndroidDriver(
  "http://127.0.0.1:4723", opts)

// pCloudy Mobile Farm
new AndroidDriver(
  "https://device.pcloudy.com/appiumcloud/wd/hub",
  caps)
What ${encoded} is

The capabilities object — your credentials, browser and OS — serialized to JSON and URL-encoded onto the query string. In readable form it is just:

{
  "browserName": "chrome",
  "pcloudy:options": {
    "userName":       "you@gientech.com",
    "accessKey":      "<YOUR_ACCESS_KEY>",
    "os":             "Windows",
    "osVersion":      "11",
    "browserVersion": "118",
    "local":          false
  }
}

encodeURIComponent() then percent-escapes it — {%7B, "%22, @%40 — so on the wire ${encoded} reads %7B%22browserName%22%3A%22chrome%22%2C%22pcloudy%3Aoptions%22…

What we actually send — Appium (this repo)

The ${encoded} form above is the Playwright/web shape. On the mobile side, the same idea wears a different name — PCloudyConfig builds a DesiredCapabilities map. Username and key come from the environment; the device is picked from the pool at run time:

{
  "appium:platformName":            "Android",
  "appium:platformVersion":         "11.0.0",
  "appium:automationName":          "uiautomator2",
  "appium:appPackage":             "com.gientechmobilebanking",
  "appium:appActivity":            ".MainActivity",
  "appium:pCloudy_ApplicationName": "app-release.apk",
  "pcloudy:options": {
    "pCloudy_Username":         "you@gientech.com",             // $PCLOUDY_USERNAME
    "pCloudy_ApiKey":           "<YOUR_API_KEY>",  // $PCLOUDY_API_KEY
    "pCloudy_DeviceFullName":    "(picked from the pool — Samsung preferred)",
    "pCloudy_WildNet":          false,
    "pCloudy_EnableVideo":       true,
    "pCloudy_EnableDeviceLogs":  true,
    "appiumVersion":            "2.0.0"
  }
}
your testcapabilities + API keypCloudy proxy booked devicevideo + logs

Prerequisites

Before you write a single line

🖥 Browser (Playwright)

  • Node 18+ and npm ci in the project
  • Playwright installed — npx playwright install chromium
  • pCloudy account with Browser Cloud entitlement
  • App reachable from the cloud host — a public or VPN-reachable URL. file:// and localhost will not work

📱 Mobile (Java Appium)

  • JDK 17 and Maven 3.6+
  • pCloudy account with device-farm entitlement
  • App binary.apk (Android) or .ipa (iOS)
  • iOS only: Apple Developer account, Ad Hoc profile, farm UDIDs registered
  • No local Appium, emulator or Xcode needed — pCloudy hosts the server
The asymmetry that surprises teams

Android needs no signing at all — any APK installs on any device. iOS refuses to launch unless the exact device UDID is inside the IPA's provisioning profile. Budget half a day for the first iOS setup; Android is minutes.

Checklist

Access & connectivity — verify before blaming your code

  • 1pCloudy access works. Sign in from a browser at https://device.pcloudy.com/.
  • 2Network access. Allow *.pcloudy.com over HTTPS (443), plus WebSocket for Playwright.
  • 3Entitlement for the product you need. Browser Cloud and mobile devices are licensed separately — an account can see one and not the other.
  • 4API key generated and stored in .env, never in code.
  • 5Auth test passes from your machine (next slide).
  • 6App uploaded (mobile) or app URL reachable (browser).
  • 7iOS: UDIDs in the provisioning profile and the IPA re-exported after any change.

Credentials

Getting the API key, and where it belongs

One page, both products — there is no separate key for browser and mobile:

https://device.pcloudy.com/pro/profile
Field on the pageUse it as
Email / API User_namePCLOUDY_USERNAME · pCloudy_Username · userName
Your API Access_KeyPCLOUDY_API_KEY · pCloudy_ApiKey · accessKey

The same two values authenticate the REST API, the Appium hub and the Playwright WebSocket endpoint. Regenerating the key on that page invalidates the old one everywhere at once.

📱 Mobile — .env
PCLOUDY_USERNAME=you@gientech.com
PCLOUDY_API_KEY=your_key_here

Read by PCloudyConfig via System.getenv, or overridden with -Dpcloudy.username / -Dpcloudy.apikey.

🖥 Browser — .env
PCLOUDY_USERNAME=you@gientech.com
PCLOUDY_API_KEY=your_key_here
PCLOUDY_WS_HOST=browser.device.pcloudy.com
PCLOUDY_BROWSER=chrome
PCLOUDY_OS=Windows
PCLOUDY_OS_VERSION=11
USE_PCLOUDY=true
Non-negotiable

The key grants full access to the farm under your account. Never commit it, paste it into a suite XML, a Javadoc comment or a shared script. Note that Appium echoes the whole capability map — including the key — into surefire failure reports, so keep target/ and report folders out of version control. If a key leaks, regenerate it immediately.

Credentials

What the Profile page looks like

Admin → Profile. My Details holds the two values every framework needs; URL Information lists every regional endpoint plus the Browser Cloud URL.

pCloudy Profile page: My Details with the email, mobile and API access key redacted, above the URL Information table listing each regional endpoint
Email, mobile and access key redacted. Regenerating the key from this page invalidates the old one everywhere at once.

Connectivity test

Prove the key works before anything else

Run these two calls before touching any test code. The first confirms your credentials and the network path; the second confirms your entitlement and returns the exact device names to put in your pool.

Raw API — works from any stack

bash — connectivity check
Windows PowerShell

curl in PowerShell is an alias for Invoke-WebRequest, which does not understand -u or -d. Call curl.exe explicitly, or use the native form:

$pair  = "$env:PCLOUDY_USERNAME:$env:PCLOUDY_API_KEY"
$basic = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
Invoke-RestMethod https://device.pcloudy.com/api/access `
  -Headers @{ Authorization = "Basic $basic" }

Configuration · 🖥 Browser

Pointing Playwright at Browser Cloud

One connection object. The credentials travel inside the capabilities, URL-encoded onto the WebSocket endpoint.

// tests/pcloudy/config.js
// Browser Cloud host from Profile > URL Information, WITHOUT the https://
const PLAYWRIGHT_WS_HOST =
  process.env.PCLOUDY_WS_HOST || 'browser.device.pcloudy.com';

const capabilities = {
  browserName: selectedBrowser,              // chrome | edge | opera
  'pcloudy:options': {
    userName:       USERNAME,
    accessKey:      API_KEY,
    os:             OS,                      // Windows | Mac
    osVersion:      OS_VERSION,              // 10 | 11 | Ventura | Sonoma
    browserVersion: BROWSER_VERSION,
    local:          false,
  },
};

const PLAYWRIGHT_WS_ENDPOINT =
  `wss://${PLAYWRIGHT_WS_HOST}/playwright?capabilities=` +
  encodeURIComponent(JSON.stringify(capabilities));

// then, instead of chromium.launch():
const browser = await chromium.connect(PLAYWRIGHT_WS_ENDPOINT);
Run it
npm run pcloudy:chrome
npm run pcloudy:edge
npm run pcloudy:smoke
Browser + OS are a pair

An unavailable combination is not a degraded run — it is an impossible booking and fails at connect time. pCloudy documents Chrome and Opera on Windows and Mac, and Edge on Windows; confirm anything else with them first.

🖥 Per pCloudy documentation

The vendor's documented Playwright setup

From pcloudy.com/docs/playwright — worth following on a first connection, because it is what their support will assume you did.

Prerequisites
  • Node 16 or higher
  • A registered pCloudy account
  • Username and access key from your Profile page
  • The project opened in your editor (their walkthrough uses VS Code)
Their four steps
  1. Put username and access key in config.js
  2. Set the WebSocket endpoint in your test file
  3. Set the OS version in pw-test.js
  4. Run it: node pw-test.js

Endpoint shape

# the docs show the pattern; the HOST comes from YOUR Profile page
wss://<your-browser-cloud-host>/playwright?capabilities=<url-encoded JSON>

# ours, from Profile > URL Information
wss://browser.device.pcloudy.com/playwright?capabilities=…
Take the host from your own Profile

The documentation's example uses a different host to the one our account is issued (browser.device.pcloudy.com). Copy yours from Profile → URL Information rather than from the docs page, or the socket will never connect.

Configuration · 📱 Mobile

Pointing Java Appium at the device farm

pCloudy's capability shape is strict. The booking block is identical on both platforms; only the app-identity capabilities differ.

Android — UiAutomator2
caps.setCapability("appium:platformName",   "Android");
caps.setCapability("appium:automationName", "uiautomator2");
caps.setCapability("appium:platformVersion","13.0.0");

// Android identifies an app by package + activity
caps.setCapability("appium:appPackage",  "com.gientechmobilebanking");
caps.setCapability("appium:appActivity", ".MainActivity");

// which uploaded build to install — TOP LEVEL
caps.setCapability("appium:pCloudy_ApplicationName",
                   "GienTechMobileBanking.apk");
iOS — XCUITest
caps.setCapability("appium:platformName",   "iOS");
caps.setCapability("appium:automationName", "XCUITest");
caps.setCapability("appium:platformVersion","18.3.0");

// iOS identifies an app by bundle ID — no activity
caps.setCapability("appium:bundleId",
                   "org.reactjs.native.example.GienTechMobileBanking");
caps.setCapability("appium:acceptAlerts", true);

// same capability, .ipa instead of .apk
caps.setCapability("appium:pCloudy_ApplicationName",
                   "GienTechMobileBanking.ipa");
// identical on both platforms — booking, auth and session options
HashMap<String,Object> pcloudy = new HashMap<>();
pcloudy.put("pCloudy_Username", USERNAME);   pcloudy.put("pCloudy_ApiKey", API_KEY);
pcloudy.put("pCloudy_DeviceFullName", deviceName);
pcloudy.put("pCloudy_EnableVideo", true);      pcloudy.put("pCloudy_EnableDeviceLogs", true);
pcloudy.put("appiumVersion", "2.0.0");
caps.setCapability("pcloudy:options", pcloudy);

// region URL from Profile > URL Information + /appiumcloud/wd/hub
String HUB = "https://device.pcloudy.com/appiumcloud/wd/hub";

new AndroidDriver(new URL(HUB), caps);
new IOSDriver(new URL(HUB), caps);      // same hub, iOS caps
The one capability that must be top level

appium:pCloudy_ApplicationName tells pCloudy which uploaded binary to install. Nested inside pcloudy:options it is silently ignored and the device runs whatever build was already there — the session starts, then every locator fails.

Leave video and device logs on

A failed cloud run you cannot see is a failed run you cannot fix. The session recording is usually faster than any log at telling you what happened on screen.

Configuration · Region

Region is not cosmetic — it is a different farm

Each region has its own device inventory, its own uploaded apps, and its own device names. A name valid in India does not exist in Singapore.

RegionURLUsed for
India (framework default)https://device.pcloudy.com📱 Mobile farm.
REST API at /api/…,
Appium hub at /appiumcloud/wd/hub
Ind-Westhttps://ind-west.pcloudy.com
USAhttps://us.pcloudy.com
Singaporehttps://sg.pcloudy.com
UAEhttps://uae.pcloudy.com
Browser Cloud — all regionshttps://browser.device.pcloudy.com🖥 Playwright / Selenium.
WSS at /playwright,
Selenium at /seleniumcloud/wd/hub

Note the asymmetry: the mobile farm is regional — five separate inventories — while Browser Cloud is a single global endpoint. Your API key works against all of them.

Set the region explicitly

Check how your framework resolves it. In this project PCLOUDY_CLOUD_URL in .env is not readPCloudyConfig takes the region only from the system property -Dpcloudy.cloud.url and otherwise defaults to India. Point at the wrong region and the symptoms are misleading: devices "missing" from your pool and uploads that "vanished", when both are simply on another farm.

Always pass the flag explicitly, or use a wrapper script that does it for you.

mvn clean test -Ppcloudy-android -Dpcloudy.cloud.url=https://device.pcloudy.com

App binaries · 📱 Mobile

Upload before you execute — console or API

pCloudy installs the app from its own cloud drive, never from your laptop. Upload once per build; the capability pCloudy_ApplicationName then refers to it by filename.

Automatable via API — no console needed

# Android — filter=apk
curl -X POST https://device.pcloudy.com/api/upload_file \
  -F "token=<TOKEN>" -F "source_type=raw" -F "filter=apk" \
  -F "file=@./GienTechMobileBanking.apk"

# iOS — filter=ipa
curl -X POST https://device.pcloudy.com/api/upload_file \
  -F "token=<TOKEN>" -F "source_type=raw" -F "filter=ipa" \
  -F "file=@./ipa-adhoc/GienTechMobileBanking.ipa"
Fits straight into CI

Because upload is an API call, a pipeline can build the app, upload it, and trigger the suite with no human in the loop. Recommended order: build → upload → verify drive listing → run.

Filter matters

filter=apk for Android, filter=ipa for iOS — the field tells pCloudy how to treat the binary. The token is a form field, not a header, and source_type=raw is required.

App binaries · The big trap

pCloudy never overwrites an upload

Upload a file whose name already exists and pCloudy keeps the original, storing yours as MyApp-1784530345.ipa. Meanwhile the tooling prints the local filename and reports success.

# what the CLI said
Upload complete: GienTechMobileBanking.ipa      ← the LOCAL name

# what was actually on the drive
GienTechMobileBanking-1784530345.ipa            ← your new build
GienTechMobileBanking.ipa                       ← the OLD build, still what runs install

Defence — always verify the drive after uploading

curl -s -X POST https://device.pcloudy.com/api/drive \
  -H "Content-Type: application/json" \
  -d '{"token":"<TOKEN>","limit":20,"filter":"all"}' | python3 -m json.tool

Delete the old file in the console before re-uploading so the replacement keeps the plain name your config expects.

iOS signing · 1 of 2

iOS will not run without device UDIDs in the profile

Android installs anywhere. iOS checks that this exact handset is listed inside the IPA's embedded provisioning profile — otherwise:

A valid provisioning profile for this executable was not found.
# or, through Appium:
SessionNotCreatedException … Message: Application Installation failed
Profile typeInstalls onUse for a device farm?
DevelopmentOnly listed UDIDs — usually your own desk devicesNo
Ad HocOnly listed UDIDs, up to 100/yearYes — the normal answer
App StoreNothing directly (TestFlight/Store only)No
Enterprise (In-House)Any device, no UDID listBest, if your org has it

iOS signing · 2 of 2

The Ad Hoc procedure, end to end

  1. Collect farm UDIDs from Utility → Device UDID: https://device.pcloudy.com/pro/device-udid. One table lists every device with its Device Full Name, version, UDID and location, with search and a Copy UDID action.
  2. Register at developer.apple.com → Devices. Devices in Processing state cannot be added to a profile yet — wait for them to go active.
  3. Check which distribution certificate you hold the private key forsecurity find-identity -v -p codesigning. Apple's portal lists a legacy "iOS Distribution" and a modern "Distribution (Xcode 11 or later)"; only the one in your keychain can sign.
  4. Generate an Ad Hoc profile for the app's exact bundle ID, tick the farm devices, name it.
  5. Re-export the IPA — no rebuild needed, export only (~1 min).
  6. Re-upload, deleting the old file first.
# ExportOptions-adhoc.plist → method ad-hoc, signingStyle manual,
# provisioningProfiles maps bundleId → profile NAME
xcodebuild -exportArchive -archivePath MyApp.xcarchive \
  -exportOptionsPlist ExportOptions-adhoc.plist -exportPath ./ipa-adhoc

# verify BEFORE uploading — 30s saves a wasted booking
codesign -dvv Payload/*.app 2>&1 | grep Authority    # expect Apple Distribution
plutil -extract ProvisionedDevices json -o - prof.plist # expect your farm UDIDs

Device availability

The pool is shared — plan for contention

A device farm is a contested resource. Any code that assumes a specific device will be free is code that fails intermittently for reasons unrelated to your app.

Booked, not ownedA session reserves the device for pcloudy.duration minutes — whether or not your tests finish.
Snapshot, not scheduleavailable_now:true is true at that instant. Two runs seconds apart can legitimately see different devices.
Race windowA device free at check time can be taken before your session request lands. Retry rather than fail.

Register a pool, in preference order

# pcloudy-android-devices.properties
android.device.1=Samsung_GalaxyF12_Android_13.0.0_53b39
android.device.2=Samsung_GalaxyA52_Android_13.0.0_fcafd
android.device.3=Samsung_GalaxyF145G_Android_13.0.0_dd226

Any device works — no signing. Keep 3+ so contention never blocks a run.

# pcloudy-ios-devices.properties
ios.device.1=Apple_iPhone14ProMax_Ios_18.3.0_eb352
ios.device.2=Apple_iPhone15ProMax_Ios_18.6.2_92d62
ios.device.3=Apple_iPhone12ProMax_Ios_16.3.1_22ba7

Only devices whose UDID is in the Ad Hoc profile may be listed — anything else fails at install.

Device names embed the OS version — and pCloudy upgrades in place

Apple_iPhone17_Ios_26.1.0_8332e silently became …_26.5.1_8332e. Matching is by exact name, so the pool entry stopped matching and selection fell through to a device we never intended. Re-check names periodically with the devices API.

Troubleshooting · 🖥 Browser

Playwright-specific failures

SymptomCauseFix
WebSocket closes immediately (code 1005)Endpoint returned a non-JSON bodySee the known bug below
Connect hangs then times outWSS blocked by corporate proxy/firewallAllow *.pcloudy.com over 443 incl. WebSocket upgrade
Browser opens, page blankApp URL not reachable from the cloud hostUse a public/VPN-reachable URL — never localhost or file://
Booking rejected instantlyBrowser/OS pair not offered on the farmUse a documented pair — Chrome/Opera on Windows or Mac, Edge on Windows
Env vars ignored on WindowsVAR=value cmd is not PowerShell syntaxUse the npm run pcloudy:* scripts (cross-env) or the .ps1 wrapper

Definition of done

You are onboarded when every connection test passes

Each step proves one link in the chain. Run them in order — the first failure tells you exactly which link is broken, instead of leaving you guessing from a red suite.

  • 1Console reachable. You can sign in and see devices from your corporate network — no proxy or IP block in the way.
  • 2Auth returns a token. GET /api/access with your email and key responds 200 with a token. Proves credentials and network path.
  • 3Devices list is non-empty. POST /api/devices returns hardware for your platform. Proves entitlement, and tells you the exact device names to put in your pool.
  • 4📱 App resolves on the drive. POST /api/drive lists the exact filename your config passes as pCloudy_ApplicationName.
  • 5🖥 WebSocket connects. Playwright reaches wss://browser.device.pcloudy.com/playwright without the socket closing — proves WSS is not blocked by a proxy.
  • 6Session is created. Appium or Playwright gets a session id back. Proves capabilities are shaped correctly and a device was actually booked.
  • 7App launches. iOS additionally proves the UDID is in the provisioning profile — the step that fails when signing is wrong.
  • 8One test asserts and passes end to end.

Demo · the whole thing in one command

One command books a device, runs the test, and reports green

zsh — gientech-mobile-banking-appium
pCloudy console · device booked, appium run On Going Live View · app launched on Samsung Galaxy F12 Session report · 0 errors, video of the passing run

Demo · 🖥 Browser Cloud · one command

Same story on the web — Playwright books a browser and runs the suite

zsh — gientech-web-banking-playwright
pCloudy console · Windows 11 / Chrome 118 session On Going Live browser · dashboard loaded on Chrome 118 / Windows 11 Transfer scenario passing · SGD 500 confirmed
01 / 18