[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #2 · 2026-09-30 · Example · Medium · for [testing](https://cdpfleet.com/cross-browser-testing)

# Emulating an iPhone on real Chrome: what changes, and what gives you away

Playwright device descriptors fix the viewport, touch and user agent — and leave five tells a site can read.

[Google Chrome](https://cdpfleet.com/docs/browsers/chrome)

Run on production on 2026-10-21: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

## The problem

You need the mobile version of a site, so you open a context with Playwright's `iPhone 15 Pro` or `Pixel 7` descriptor. The page renders the mobile layout. But is the browser now convincing as a phone? A bot check can look at the network (TLS, client hints) and at JavaScript APIs the descriptor doesn't touch. Let's compare a desktop context, an iPhone context and a Pixel context in the **same** Chrome session.

## What we used, and why

| What | Why |
| --- | --- |
| `chrome` | Real Google Chrome, the most common desktop browser. One session serves all three contexts. |
| `devices['iPhone 15 Pro']`, `devices['Pixel 7']` | Playwright's descriptors: user agent, viewport, screen, device scale factor, `isMobile`, `hasTouch`. (Playwright for Java has no registry, so the Java version spells the same values out.) |
| tls.peet.ws | Shows what the network sees: the user agent, client-hint headers and TLS fingerprint. |
| `page.evaluate` | Reads what page scripts see: viewport, touch points, pointer type, `navigator.platform`, `navigator.userAgentData`. |

## How it works

1.  Launch one Chrome session.
2.  Open three contexts: plain, iPhone 15 Pro, Pixel 7.
3.  In each, load tls.peet.ws and read the JavaScript-visible device signals.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/02-mobile-emulation), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium, devices } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

const res = await fetch('https://starter.cdpfleet.com/chrome/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: true }),
});
if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });

// What a page can see about the device, plus what the network sees (tls.peet.ws).
async function inspect(context) {
  const page = await context.newPage();
  const fp = await (await page.goto('https://tls.peet.ws/api/all', { timeout: 60000 })).json();
  const js = await page.evaluate(() => ({
    viewport: `${innerWidth}x${innerHeight}`,
    screen: `${screen.width}x${screen.height}`,
    device_pixel_ratio: devicePixelRatio,
    max_touch_points: navigator.maxTouchPoints,
    coarse_pointer: matchMedia('(pointer: coarse)').matches,
    platform: navigator.platform,
    ua_data_mobile: navigator.userAgentData?.mobile ?? null,
    ua_data_platform: navigator.userAgentData?.platform ?? null,
  }));
  const headers = fp.http2.sent_frames.find((f) => f.frame_type === 'HEADERS').headers;
  const header = (name) => headers.find((h) => h.startsWith(`${name}: `))?.slice(name.length + 2) ?? null;
  await page.close();
  return {
    user_agent: fp.user_agent,
    ...js,
    sec_ch_ua_mobile: header('sec-ch-ua-mobile'),
    sec_ch_ua_platform: header('sec-ch-ua-platform'),
    ja4: fp.tls.ja4,
    akamai_h2_hash: fp.http2.akamai_fingerprint_hash,
  };
}

try {
  const desktop = await browser.newContext();
  const iphone = await browser.newContext({ ...devices['iPhone 15 Pro'] });
  const pixel = await browser.newContext({ ...devices['Pixel 7'] });
  console.log(JSON.stringify({
    desktop: await inspect(desktop),
    'iPhone 15 Pro': await inspect(iphone),
    'Pixel 7': await inspect(pixel),
  }, null, 2));
} finally {
  await browser.close();
}
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]

res = requests.post("https://starter.cdpfleet.com/chrome/session", headers={"x-api-key": KEY},
                    json={"proxy": os.environ["PROXY_URL"], "headless": True}, timeout=60)
res.raise_for_status()
ws_url = res.json()["wsUrl"]

PAGE_SIGNALS = """() => ({
  viewport: `${innerWidth}x${innerHeight}`,
  screen: `${screen.width}x${screen.height}`,
  device_pixel_ratio: devicePixelRatio,
  max_touch_points: navigator.maxTouchPoints,
  coarse_pointer: matchMedia('(pointer: coarse)').matches,
  platform: navigator.platform,
  ua_data_mobile: navigator.userAgentData ? navigator.userAgentData.mobile : null,
  ua_data_platform: navigator.userAgentData ? navigator.userAgentData.platform : null,
})"""

def inspect(context):
    """What a page can see about the device, plus what the network sees (tls.peet.ws)."""
    page = context.new_page()
    fp = page.goto("https://tls.peet.ws/api/all", timeout=60000).json()
    js = page.evaluate(PAGE_SIGNALS)
    headers = next(f for f in fp["http2"]["sent_frames"] if f["frame_type"] == "HEADERS")["headers"]
    header = lambda name: next((h[len(name) + 2:] for h in headers if h.startswith(f"{name}: ")), None)
    page.close()
    return {
        "user_agent": fp["user_agent"],
        **js,
        "sec_ch_ua_mobile": header("sec-ch-ua-mobile"),
        "sec_ch_ua_platform": header("sec-ch-ua-platform"),
        "ja4": fp["tls"]["ja4"],
        "akamai_h2_hash": fp["http2"]["akamai_fingerprint_hash"],
    }

with sync_playwright() as p:
    browser = p.chromium.connect(ws_url, headers={"x-api-key": KEY})
    try:
        print(json.dumps({
            "desktop": inspect(browser.new_context()),
            "iPhone 15 Pro": inspect(browser.new_context(**p.devices["iPhone 15 Pro"])),
            "Pixel 7": inspect(browser.new_context(**p.devices["Pixel 7"])),
        }, indent=2))
    finally:
        browser.close()
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.Map;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");

  static final String PAGE_SIGNALS = """
      () => ({
        viewport: `${innerWidth}x${innerHeight}`,
        screen: `${screen.width}x${screen.height}`,
        device_pixel_ratio: devicePixelRatio,
        max_touch_points: navigator.maxTouchPoints,
        coarse_pointer: matchMedia('(pointer: coarse)').matches,
        platform: navigator.platform,
        ua_data_mobile: navigator.userAgentData ? navigator.userAgentData.mobile : null,
        ua_data_platform: navigator.userAgentData ? navigator.userAgentData.platform : null,
      })""";

  // Playwright for Java has no device registry: these are its iPhone 15 Pro and Pixel 7.
  static Browser.NewContextOptions device(String ua, int w, int h, int sw, int sh, double scale) {
    return new Browser.NewContextOptions().setUserAgent(ua).setViewportSize(w, h).setScreenSize(sw, sh)
        .setDeviceScaleFactor(scale).setIsMobile(true).setHasTouch(true);
  }

  // What a page can see about the device, plus what the network sees (tls.peet.ws).
  static JsonObject inspect(BrowserContext context) {
    Page page = context.newPage();
    JsonObject fp = JsonParser.parseString(page.navigate("https://tls.peet.ws/api/all",
        new Page.NavigateOptions().setTimeout(60000)).text()).getAsJsonObject();
    JsonObject js = new Gson().toJsonTree(page.evaluate(PAGE_SIGNALS)).getAsJsonObject();
    JsonArray headers = null;
    for (JsonElement f : fp.getAsJsonObject("http2").getAsJsonArray("sent_frames")) {
      if (f.getAsJsonObject().get("frame_type").getAsString().equals("HEADERS")) headers = f.getAsJsonObject().getAsJsonArray("headers");
    }
    page.close();
    JsonObject out = new JsonObject();
    out.add("user_agent", fp.get("user_agent"));
    for (String k : js.keySet()) out.add(k, js.get(k));
    out.addProperty("sec_ch_ua_mobile", header(headers, "sec-ch-ua-mobile"));
    out.addProperty("sec_ch_ua_platform", header(headers, "sec-ch-ua-platform"));
    out.add("ja4", fp.getAsJsonObject("tls").get("ja4"));
    out.add("akamai_h2_hash", fp.getAsJsonObject("http2").get("akamai_fingerprint_hash"));
    return out;
  }

  static String header(JsonArray headers, String name) {
    for (JsonElement h : headers) if (h.getAsString().startsWith(name + ": ")) return h.getAsString().substring(name.length() + 2);
    return null;
  }

  public static void main(String[] args) throws Exception {
    String body = "{\"proxy\": " + new Gson().toJson(System.getenv("PROXY_URL")) + ", \"headless\": true}";
    HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chrome/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();

    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        JsonObject out = new JsonObject();
        out.add("desktop", inspect(browser.newContext()));
        out.add("iPhone 15 Pro", inspect(browser.newContext(device(
            "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.4 Mobile/15E148 Safari/604.1",
            393, 659, 393, 852, 3))));
        out.add("Pixel 7", inspect(browser.newContext(device(
            "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36",
            412, 839, 412, 915, 2.625))));
        System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
      } finally {
        browser.close();
      }
    }
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chrome/session",
    new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = true });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });

const string PageSignals = """
    () => ({
      viewport: `${innerWidth}x${innerHeight}`,
      screen: `${screen.width}x${screen.height}`,
      device_pixel_ratio: devicePixelRatio,
      max_touch_points: navigator.maxTouchPoints,
      coarse_pointer: matchMedia('(pointer: coarse)').matches,
      platform: navigator.platform,
      ua_data_mobile: navigator.userAgentData ? navigator.userAgentData.mobile : null,
      ua_data_platform: navigator.userAgentData ? navigator.userAgentData.platform : null,
    })
    """;

// What a page can see about the device, plus what the network sees (tls.peet.ws).
async Task<JsonObject> Inspect(IBrowserContext context)
{
    var page = await context.NewPageAsync();
    var fp = JsonNode.Parse(await (await page.GotoAsync("https://tls.peet.ws/api/all", new() { Timeout = 60000 }))!.TextAsync())!;
    var js = JsonNode.Parse((await page.EvaluateAsync<JsonElement>(PageSignals)).GetRawText())!.AsObject();
    var headers = fp["http2"]!["sent_frames"]!.AsArray().First(f => (string?)f!["frame_type"] == "HEADERS")!["headers"]!.AsArray()
        .Select(h => (string)h!).ToList();
    string? Header(string name) => headers.FirstOrDefault(h => h.StartsWith(name + ": "))?[(name.Length + 2)..];
    await page.CloseAsync();
    var outp = new JsonObject { ["user_agent"] = fp["user_agent"]!.DeepClone() };
    foreach (var (k, v) in js) outp[k] = v?.DeepClone();
    outp["sec_ch_ua_mobile"] = Header("sec-ch-ua-mobile");
    outp["sec_ch_ua_platform"] = Header("sec-ch-ua-platform");
    outp["ja4"] = fp["tls"]!["ja4"]!.DeepClone();
    outp["akamai_h2_hash"] = fp["http2"]!["akamai_fingerprint_hash"]!.DeepClone();
    return outp;
}

try
{
    var result = new JsonObject
    {
        ["desktop"] = await Inspect(await browser.NewContextAsync()),
        ["iPhone 15 Pro"] = await Inspect(await browser.NewContextAsync(playwright.Devices["iPhone 15 Pro"])),
        ["Pixel 7"] = await Inspect(await browser.NewContextAsync(playwright.Devices["Pixel 7"])),
    };
    Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
    await browser.CloseAsync();
}
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

func launch(name string, options map[string]any) (map[string]any, error) {
	body, _ := json.Marshal(options)
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
	}
	var session map[string]any
	return session, json.NewDecoder(res.Body).Decode(&session)
}

const pageSignals = `() => ({
  viewport: innerWidth + 'x' + innerHeight,
  screen: screen.width + 'x' + screen.height,
  device_pixel_ratio: devicePixelRatio,
  max_touch_points: navigator.maxTouchPoints,
  coarse_pointer: matchMedia('(pointer: coarse)').matches,
  platform: navigator.platform,
  ua_data_mobile: navigator.userAgentData ? navigator.userAgentData.mobile : null,
  ua_data_platform: navigator.userAgentData ? navigator.userAgentData.platform : null,
})`

type peet struct {
	UserAgent string `json:"user_agent"`
	TLS       struct {
		JA4 string `json:"ja4"`
	} `json:"tls"`
	HTTP2 struct {
		AkamaiFingerprintHash string `json:"akamai_fingerprint_hash"`
		SentFrames            []struct {
			FrameType string   `json:"frame_type"`
			Headers   []string `json:"headers"`
		} `json:"sent_frames"`
	} `json:"http2"`
}

// What a page can see about the device, plus what the network sees (tls.peet.ws).
func inspect(context playwright.BrowserContext) map[string]any {
	page, _ := context.NewPage()
	defer page.Close()
	res, err := page.Goto("https://tls.peet.ws/api/all", playwright.PageGotoOptions{Timeout: playwright.Float(60000)})
	if err != nil {
		log.Fatal(err)
	}
	var fp peet
	if err := res.JSON(&fp); err != nil {
		log.Fatal(err)
	}
	js, err := page.Evaluate(pageSignals)
	if err != nil {
		log.Fatal(err)
	}
	out := js.(map[string]any)
	header := func(name string) any {
		for _, f := range fp.HTTP2.SentFrames {
			for _, h := range f.Headers {
				if f.FrameType == "HEADERS" && strings.HasPrefix(h, name+": ") {
					return h[len(name)+2:]
				}
			}
		}
		return nil
	}
	out["user_agent"] = fp.UserAgent
	out["sec_ch_ua_mobile"] = header("sec-ch-ua-mobile")
	out["sec_ch_ua_platform"] = header("sec-ch-ua-platform")
	out["ja4"] = fp.TLS.JA4
	out["akamai_h2_hash"] = fp.HTTP2.AkamaiFingerprintHash
	return out
}

// Playwright's device descriptors as new-context options.
func device(d *playwright.DeviceDescriptor) playwright.BrowserNewContextOptions {
	return playwright.BrowserNewContextOptions{
		UserAgent: playwright.String(d.UserAgent), Viewport: d.Viewport, Screen: d.Screen,
		DeviceScaleFactor: playwright.Float(d.DeviceScaleFactor), IsMobile: playwright.Bool(d.IsMobile), HasTouch: playwright.Bool(d.HasTouch),
	}
}

func main() {
	session, err := launch("chrome", map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": true})
	if err != nil {
		log.Fatal(err)
	}
	pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
	if err != nil {
		log.Fatal(err)
	}
	defer pw.Stop()
	browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
	if err != nil {
		log.Fatal(err)
	}
	defer browser.Close()

	desktop, _ := browser.NewContext()
	iphone, _ := browser.NewContext(device(pw.Devices["iPhone 15 Pro"]))
	pixel, _ := browser.NewContext(device(pw.Devices["Pixel 7"]))
	out, _ := json.MarshalIndent(map[string]any{
		"desktop":       inspect(desktop),
		"iPhone 15 Pro": inspect(iphone),
		"Pixel 7":       inspect(pixel),
	}, "", "  ")
	fmt.Println(string(out))
}
```

## What we got

| Context | User agent | Viewport | DPR | Touch | navigator.platform | userAgentData | JA4 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| desktop | Mozilla/5.0 (X11; Linux x86\_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/155.0.0.0 Safari/537.36 | 1280x720 | 1 | 0 | Linux x86\_64 | Linux | t13d1517h2\_8daaf6152771\_cb7bf5808d99 |
| iPhone 15 Pro | Mozilla/5.0 (iPhone; CPU iPhone OS 17\_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.4 Mobile/15E148 Safari/604.1 | 980x1644 | 3 | 1 | Linux x86\_64 | iOS | t13d1517h2\_8daaf6152771\_cb7bf5808d99 |
| Pixel 7 | Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36 | 981x1996 | 2.625 | 1 | Linux x86\_64 | Android | t13d1517h2\_8daaf6152771\_cb7bf5808d99 |

From the Node.js run on 2026-10-21. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
{
  "desktop": {
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/155.0.0.0 Safari/537.36",
    "viewport": "1280x720",
    "screen": "1280x720",
    "device_pixel_ratio": 1,
    "max_touch_points": 0,
    "coarse_pointer": false,
    "platform": "Linux x86_64",
    "ua_data_mobile": false,
    "ua_data_platform": "Linux",
    "sec_ch_ua_mobile": "?0",
    "sec_ch_ua_platform": "\"Linux\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  },
  "iPhone 15 Pro": {
    "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.4 Mobile/15E148 Safari/604.1",
    "viewport": "980x1644",
    "screen": "393x852",
    "device_pixel_ratio": 3,
    "max_touch_points": 1,
    "coarse_pointer": true,
    "platform": "Linux x86_64",
    "ua_data_mobile": true,
    "ua_data_platform": "iOS",
    "sec_ch_ua_mobile": "?1",
    "sec_ch_ua_platform": "\"iOS\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  },
  "Pixel 7": {
    "user_agent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36",
    "viewport": "981x1996",
    "screen": "412x915",
    "device_pixel_ratio": 2.625,
    "max_touch_points": 1,
    "coarse_pointer": true,
    "platform": "Linux x86_64",
    "ua_data_mobile": true,
    "ua_data_platform": "Android",
    "sec_ch_ua_mobile": "?1",
    "sec_ch_ua_platform": "\"Android\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  }
}
```

## Takeaways

-   **What changes:** the user agent, viewport, screen size, device pixel ratio, touch points, `pointer: coarse`, and the `sec-ch-ua-mobile` / `sec-ch-ua-platform` client hints.
-   **Tell 1 — TLS:** the JA4 and HTTP/2 fingerprints are Chrome's in all three contexts. An "iPhone browser" with Chrome's TLS is exactly what fingerprinting vendors look for.
-   **Tell 2 — client hints on an iPhone user agent:** WebKit-based browsers never send `sec-ch-ua-*` headers or expose `navigator.userAgentData`; the emulated iPhone does both (and says `platform: iOS`).
-   **Tell 3 — `navigator.platform` stays `Linux x86_64`** in both mobile contexts. A real iPhone says `iPhone`, a real Pixel `Linux armv81`.
-   **Tell 4 — version skew on Android:** the Pixel descriptor's user agent says Chrome 148 while the browser (and its client hints) is Chrome 155 (2026-10-21) — and the gap grows with every release, since the descriptor is fixed in your Playwright version.
-   **Tell 5 — headless:** the desktop context's user agent says `HeadlessChrome`. Use `headless: false` (2 threads) when that matters, or set a user agent.
-   Emulation is fine for layout and responsive testing. To look like a real phone to a bot check, use a real device — cdpfleet's hardware Android phones and iOS WebView are in early access.

---
Source: https://cdpfleet.com/docs/cases/mobile-emulation · Markdown: https://cdpfleet.com/docs/cases/mobile-emulation.md · All docs for AI tools: https://cdpfleet.com/llms.txt
