GienTech · Test Automation Enablement
What every team needs before their Playwright browser tests or Java Appium mobile tests will run on real cloud devices.
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
Your test code does not change. Only where the browser or device lives, and how the session is requested.
// local
chromium.launch()
// pCloudy Browser Cloud
chromium.connect(
`wss://browser.device.pcloudy.com/playwright
?capabilities=${encoded}`)
// local
new AndroidDriver(
"http://127.0.0.1:4723", opts)
// pCloudy Mobile Farm
new AndroidDriver(
"https://device.pcloudy.com/appiumcloud/wd/hub",
caps)
${encoded} isThe 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…
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"
}
}
Prerequisites
npm ci in the projectnpx playwright install chromiumfile:// and localhost will not work.apk (Android) or .ipa (iOS)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
https://device.pcloudy.com/.*.pcloudy.com over HTTPS (443), plus
WebSocket for Playwright..env, never in code.Credentials
One page, both products — there is no separate key for browser and mobile:
https://device.pcloudy.com/pro/profile
| Field on the page | Use it as |
|---|---|
| Email / API User_name | PCLOUDY_USERNAME · pCloudy_Username · userName |
| Your API Access_Key | PCLOUDY_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.
.envPCLOUDY_USERNAME=you@gientech.com
PCLOUDY_API_KEY=your_key_here
Read by PCloudyConfig via
System.getenv, or overridden with
-Dpcloudy.username / -Dpcloudy.apikey.
.envPCLOUDY_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
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
Admin → Profile. My Details holds the two values every framework needs; URL Information lists every regional endpoint plus the Browser Cloud URL.
Connectivity test
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.
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
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);
npm run pcloudy:chrome
npm run pcloudy:edge
npm run pcloudy:smoke
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
From pcloudy.com/docs/playwright — worth following on a first connection, because it is
what their support will assume you did.
config.jspw-test.jsnode pw-test.js# 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=…
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
pCloudy's capability shape is strict. The booking block is identical on both platforms; only the app-identity capabilities differ.
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");
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
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.
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
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.
| Region | URL | Used for |
|---|---|---|
| India (framework default) | https://device.pcloudy.com | 📱 Mobile farm. REST API at /api/…,Appium hub at /appiumcloud/wd/hub |
| Ind-West | https://ind-west.pcloudy.com | |
| USA | https://us.pcloudy.com | |
| Singapore | https://sg.pcloudy.com | |
| UAE | https://uae.pcloudy.com | |
| Browser Cloud — all regions | https://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.
Check how your framework resolves it. In this project PCLOUDY_CLOUD_URL in
.env is not read — PCloudyConfig 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
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.
# 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"
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=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
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
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
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 type | Installs on | Use for a device farm? |
|---|---|---|
| Development | Only listed UDIDs — usually your own desk devices | No |
| Ad Hoc | Only listed UDIDs, up to 100/year | Yes — the normal answer |
| App Store | Nothing directly (TestFlight/Store only) | No |
| Enterprise (In-House) | Any device, no UDID list | Best, if your org has it |
iOS signing · 2 of 2
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.Processing state
cannot be added to a profile yet — wait for them to go active.security 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.# 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
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.
pcloudy.duration minutes — whether or not your tests finish.available_now:true is true at that
instant. Two runs seconds apart can legitimately see different devices.# 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.
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
| Symptom | Cause | Fix |
|---|---|---|
| WebSocket closes immediately (code 1005) | Endpoint returned a non-JSON body | See the known bug below |
| Connect hangs then times out | WSS blocked by corporate proxy/firewall | Allow *.pcloudy.com over 443 incl. WebSocket upgrade |
| Browser opens, page blank | App URL not reachable from the cloud host | Use a public/VPN-reachable URL — never localhost or file:// |
| Booking rejected instantly | Browser/OS pair not offered on the farm | Use a documented pair — Chrome/Opera on Windows or Mac, Edge on Windows |
| Env vars ignored on Windows | VAR=value cmd is not PowerShell syntax | Use the npm run pcloudy:* scripts (cross-env) or the .ps1 wrapper |
Definition of done
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.
GET /api/access with your email and key responds 200 with a token.
Proves credentials and network path.POST /api/devices returns hardware for your platform. Proves entitlement, and tells you
the exact device names to put in your pool.POST /api/drive lists the exact filename your config passes as
pCloudy_ApplicationName.wss://browser.device.pcloudy.com/playwright without the socket closing — proves WSS is
not blocked by a proxy.Demo · the whole thing in one command
Demo · 🖥 Browser Cloud · one command