A page's environment
A Chrome page says a lot about where it’s running: the size of the
screen and the window, the language, the time zone, whether it’s a phone,
where in the world it is. You can set all of that, for one page, for a
context’s pages or for every page of a browser, with the names Playwright
gives new_context():
import netweir
async with netweir.browser() as browser: context = await browser.new_context( locale="de-DE", timezone_id="Europe/Berlin", viewport={"width": 1366, "height": 768}, screen={"width": 1920, "height": 1080}, color_scheme="dark", ) page = await context.new_page() await page.goto("https://example.com/")Or as a phone:
page = await browser.new_page(device="Galaxy S23")What matters is that every part of the page agrees. A site doesn’t only
read navigator.language: it compares it with Intl, with the
Accept-Language header of every request, with what a worker says and
with what a frame from another site says. Netweir sets each setting
through Chrome itself, in the page, in each of its frames and in each of
its workers, so they all say the same thing. The
design note has what was measured, and the few
things Chrome won’t let anyone change.
Settings
Section titled “Settings”| Setting | Does |
|---|---|
device |
a preset, below: a desktop’s screen, or a phone’s screen, viewport, touch and identity. The other settings change it |
viewport |
{"width", "height"}: innerWidth and innerHeight. The window around it is a real one, so outerWidth and outerHeight are what a window that size has. None, as Playwright’s no_viewport, leaves the window as Chrome opens it. One too big for its screen raises ValueError |
screen |
{"width", "height"}: screen.width and screen.height, and what media queries say about the device |
device_scale_factor |
devicePixelRatio. A whole number, except for a whole browser (see Where to set it) |
is_mobile |
a phone’s layout, with the viewport meta tag honoured. Needs a phone’s device or user_agent |
has_touch |
a touch screen: touch events, navigator.maxTouchPoints and (pointer: coarse). Only for a phone: Chrome makes a touch device touch-only, which no laptop is, so a desktop with it raises ValueError, as a phone without it does |
locale |
such as "de-DE": Intl, dates and numbers, navigator.language and Accept-Language |
timezone_id |
such as "Europe/Berlin": Intl, Date and getTimezoneOffset() |
geolocation |
{"latitude", "longitude", "accuracy"}, for navigator.geolocation once the page may use it |
permissions |
permissions granted to every origin, such as ["geolocation"] |
color_scheme |
"light", "dark" or "no-preference", for prefers-color-scheme |
reduced_motion |
"reduce" or "no-preference", for prefers-reduced-motion |
forced_colors |
"active" or "none", for forced-colors |
contrast |
"more" or "no-preference", for prefers-contrast |
extra_http_headers |
a dict of headers sent with every request, after Chrome’s own |
offline |
True takes the page off the network: requests fail and navigator.onLine is false |
user_agent |
the running Chrome’s user agent, perhaps for another system; the client hints change with it |
init_scripts |
scripts run in every document, before its own scripts |
Unset settings are Chrome’s own. A page with no viewport shows 1280 by
720, or fills the screen’s work area if that’s smaller, or takes the size
a --window-size switch gives; a headless Chrome Netweir starts has a
1920 by 1080 screen, which every window fits on. Screens look like a
person’s: on a Mac, the menu bar and the Dock are taken from the work
area, and a MacBook’s own screen (the HiDPI presets) is the “Built-in
Retina Display”, 30 bits deep; on Windows the taskbar’s 48 pixels are,
as Windows 11 shows them.
Devices
Section titled “Devices”netweir.devices lists the presets, each as the keyword arguments it
stands for, so browser.new_context(**netweir.devices["Galaxy S23"]) and
browser.new_context(device="Galaxy S23") are the same.
| Preset | Screen | Ratio | Viewport |
|---|---|---|---|
Desktop 1920x1080 |
1920 × 1080 | 1 | as unset |
Desktop 2560x1440 |
2560 × 1440 | 1 | as unset |
Desktop 1366x768 |
1366 × 768 | 1 | as unset: it fills the screen |
Desktop 1440x900 HiDPI |
1440 × 900 | 2 | as unset |
Desktop 1512x982 HiDPI |
1512 × 982 | 2 | as unset |
Desktop 1536x960 125% |
1536 × 960 | 1.25 | as unset; the whole browser’s only |
Galaxy S21 |
360 × 800 | 3 | 360 × 724 |
Galaxy S22 |
360 × 780 | 3 | 360 × 704 |
Galaxy S23 |
360 × 780 | 3 | 360 × 704 |
A desktop preset changes the screen and nothing else: the page is still the system Chrome runs on, since its fonts and its graphics card would say so whatever its user agent claimed.
Desktop 1536x960 125% is the commonest kind of Windows laptop, a
1920 by 1200 panel at 125%, as measured on a Dell XPS. A ratio of 1.25
can only be the whole browser’s, so give it at launch,
netweir.browser(environment={"device": "Desktop 1536x960 125%"}); a
page or context given it in a browser launched without it raises
ValueError. Not on Windows, though, where any ratio that isn’t whole
raises ValueError at launch: headless Chrome there shows a screen
smaller than its own work area (1228 × 768 around a 1536 × 912 one).
It’s for showing a Windows laptop from a Mac or Linux, with a Windows
user agent to match.
A phone is an Android phone running Chrome, in every client hint and
every header: Sec-CH-UA-Mobile: ?1, the platform "Android", its model
(SM-S911B for the S23), a phone’s user agent and navigator.platform,
eight cores, five touch points. Clicks become taps, scrolling becomes a
finger dragging the page, and a key pressed is what an on-screen
keyboard sends (an “Unidentified” key, then the text), since a phone has
no mouse and no keys; hover() on a phone raises ValueError, as
there’s nothing to hover with. A page that says is_mobile=False, in a
phone’s context, takes nothing of the phone.
A phone is most a phone when it’s the whole browser’s,
netweir.browser(environment={"device": "Galaxy S23"}): then it has no
PDF viewer (no plugins, as a phone’s Chrome has none), one screen, and
service and shared workers that say they’re the phone too. A phone given
to one page or context can’t have those, and Netweir says so once.
There’s no Pixel and no iPhone. A Pixel’s pixel ratio is 2.625 or 2.75, and headless Chrome can only make screens with whole ratios, so a frame from another site would see a different one. An iPhone runs Safari’s engine, which nothing about a Chrome page can pass for.
Where to set it
Section titled “Where to set it”- A page:
browser.new_page(locale=..., ...), a page with a context of its own. - A context’s pages:
browser.new_context(...), andcontext.new_page(...)changes it for one page. A page can’t have atimezone_idorlocaleof its own in a shared context: Chrome keeps one of each per renderer process, and pages of one site share one, socontext.new_page(timezone_id=...)raisesValueErrorunless it’s the context’s. Open the page withbrowser.new_page()instead. - Every page of a browser:
netweir.browser(environment={...}), with the same keys. Set here, the screen and pixel ratio are Chrome’s own from the start, the language is Chrome’s own (so it sends it exactly as a person’s Chrome would, every header in its place), and the user agent reaches service and shared workers, which nothing set later does. A pixel ratio that isn’t a whole number, such as Windows’ 1.25, can only be set here. Chrome’s own list for a context it makes can add its own language:de-DEshows asde-DE,de,en-US,en, everywhere alike, as it would for a German speaker whose Chrome is in English.
A screen, a pixel ratio or a phone needs a headless Chrome Netweir
started. A real screen can’t be changed, so with headless=False, or a
browser you connected to, they raise ValueError; a viewport resizes
the real window.
Changing it later
Section titled “Changing it later”await page.set_viewport_size({"width": 1024, "height": 768})await page.emulate_media(color_scheme="dark", reduced_motion="reduce")await page.emulate_media(media="print", color_scheme="null")await page.set_extra_http_headers({"X-Team": "data"})await page.add_init_script("window.started = Date.now()")await page.add_init_script(path="setup.js")await page.set_geolocation({"latitude": 51.5, "longitude": -0.12})await page.set_offline(True)emulate_media takes media, color_scheme, reduced_motion,
forced_colors and contrast as Playwright’s does: one left out stays
as it is, and "null" takes its emulation away.
page.viewport_size is the viewport Netweir last gave the window. A
context has set_geolocation, set_offline, set_extra_http_headers
and add_init_script too, for the pages it has and the ones it opens
later.
Permissions and where you are
Section titled “Permissions and where you are”A page asking for its position goes through Chrome’s own permission
check, as it would for a person: it needs the geolocation permission
for its origin, and a position.
page = await browser.new_page(geolocation={"latitude": 48.86, "longitude": 2.35})await page.grant_permissions(["geolocation"], origin="https://example.com")grant_permissions sets only the permissions you name; the rest stay
what a new person’s Chrome has ("prompt"). clear_permissions() takes
back what was granted. A page that asks without the permission is turned
down, as if the person had clicked Block, and the profile remembers that
for the origin.
Init scripts
Section titled “Init scripts”An init script runs in the page and in each of its frames, those from other sites included, before any of the document’s own scripts, in the page’s own JavaScript world. It doesn’t run in workers: Chrome has no way to run anything in a worker before its own script.
In a crawl
Section titled “In a crawl”Settings.browser_environment gives every page the crawl opens in Chrome
an environment:
settings = netweir.Settings( browser="on_block", browser_environment={"device": "Galaxy S23", "locale": "pt-BR", "timezone_id": "America/Sao_Paulo"},)Chrome is started with it, as netweir.browser(environment=...) starts
it. When Chrome gets through a block and hands the site’s cookies to the
HTTP client, the client sends the same Accept-Language, user agent and
client hints Chrome sent, so the site sees one visitor rather than two.
What it can’t hide
Section titled “What it can’t hide”Chrome lets DevTools change a great deal, but not everything:
- A worker’s
navigator.platformis always the system Chrome runs on. A phone’s page and frames sayLinux armv81; its workers sayMacIntelon a Mac. - A phone’s window, seen from a frame from another site. Chrome won’t
make a window narrower than 500 pixels, and such a frame reads the real
window’s size (
outerWidth), where the page reads the phone’s. - What the hardware says. A phone preset still renders with the
machine’s graphics card, which WebGL names, and with its fonts. Desktop
APIs a phone’s Chrome hasn’t are still there (
navigator.hid,navigator.serial,navigator.keyboard,showOpenFilePicker,getScreenDetails,screen.isExtended), andnavigator.connectionhas notype, which a phone’s has. - One page’s phone besides: its screen sits beside the browser’s (so
screen.isExtendedis true), it keeps the browser’s PDF viewer and plugins, and its service and shared workers keep the browser’s user agent (their client hints agree with it). Their language, clock, headers and network do follow the page. - One page’s or context’s own language is sent through DevTools’
override, which puts
Accept-Languageearly among every request’s headers, where Chrome puts its own last; and a service worker’s own script is fetched with Chrome’s language, outside every page. Give the language to the whole browser where that matters. - More than one screen. Pages with different screens in one browser
put Chrome’s windows on screens side by side, as on a desk with two
monitors, so
screen.isExtendedis true. One screen for the whole browser, set at launch, leaves it false. - A service worker kept for a page’s language, clock, headers or phone stays running while the page is open, where Chrome would stop an idle one after about 30 seconds.
- Windows’ invisible window border: a restored window on Windows is
said to have one (not measured), a maximised one has none (measured), and a headless
window has none either way, so
outerWidthisinnerWidth. - A permission prompt is turned down at once, where a person would take a moment.
- A worker’s script request has its User-Agent early among its
headers (after
Accept, where Chrome sends it afterReferer): the user agent override every page has, to keep Chrome’s client hints, puts it there, and a server that reads header order can see it.netweir.browser(worker_header_order=True)(orSettings.browser_worker_header_orderin a crawl) puts it back, at a price: every request of every page is paused on its way out, which took a page with 300 subresources from 93 ms to 118, and one with frames and workers from 200 ms to 292.