# Zyte API browser automation

You can use browser automation through Zyte API to get [browser-rendered
HTML](#zapi-browser-html), [screenshots](#zapi-screenshot), or
both.

For browser requests, Zyte API also supports:

- [Actions](#zapi-actions),
  [network capture](#zapi-network-capture),
  [request headers](#zapi-set-browser-headers),
  [redirection](#zapi-browser-redirection),
  and [toggling JavaScript](#zapi-javascript).
- [Geolocation](features.md#zapi-geolocation),
  [IP type](features.md#zapi-ip-type),
  [cookies](features.md#zapi-cookies),
  [sessions](features.md#zapi-sessions),
  [redirection](#zapi-browser-redirection),
  [response headers](features.md#zapi-headers),
  and [metadata](features.md#zapi-metadata).

Unlike [HTTP requests](http.md#zapi-http), browser requests do not support:

- An HTTP request method, body, or header other than [Referer](#zapi-set-browser-headers).
  > [!NOTE]
  > This only affects the initial request. During a browser request,
  > as a result of redirection, JavaScript, or [actions](#zapi-actions), additional requests may be sent with no
  > limitation on method, body or headers, and may be [captured](#zapi-network-capture).
- Returning non-HTML response data, other than a screenshot.

> [!TIP]
> For full control over the initial request, or to get response data
> in a format other than HTML or a screenshot, use [CDP](cdp.md#cdp) instead.

All browser request features are also available for [automatic extraction](extract/index.md#zapi-extract) requests that use a browser request as [extraction
source](extract/index.md#zapi-extract-from).

## Browser HTML

Browser HTML is the HTML representation of the [Document Object Model](https://en.wikipedia.org/wiki/Document_Object_Model) (DOM)
of a webpage after it has been rendered in a browser.

To get browser HTML, set the [browserHtml](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/browserHtml) request field to
`true`. The [browserHtml](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/response/200/browserHtml) response field is the browser HTML
as a string.

> [!NOTE]
> By default, [iframes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) in [browserHtml](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/response/200/browserHtml) are empty. Set
> [includeIframes](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/includeIframes) to `true` to embed iframe content in
> `browserHtml`.
> 
> To access content from the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM), check out the corresponding
> example under [Actions](#zapi-actions).

See also [HTML and browser HTML](http.md#zapi-raw-vs-browser).

### Example

> [!NOTE]
> Install and configure [code example requirements](https://docs.pytest.org/en/stable/example/index.html#examples) and
> the [Zyte CA certificate](../../misc/ca.md#ca) to run the example below.

### CLI client

input.jsonl
```json
{"url": "https://toscrape.com", "browserHtml": true}
```

```shell
zyte-api input.jsonl \
    | jq --raw-output .browserHtml
```

### curl

input.json
```json
{
    "url": "https://toscrape.com",
    "browserHtml": true
}
```

```shell
curl \
    --user YOUR_ZYTE_API_KEY: \
    --header 'Content-Type: application/json' \
    --data @input.json \
    --compressed \
    https://api.zyte.com/v1/extract \
    | jq --raw-output .browserHtml
```

### Proxy mode

```shell
curl \
    --proxy api.zyte.com:8011 \
    --proxy-user YOUR_ZYTE_API_KEY: \
    --compressed \
    -H "Zyte-Browser-Html: true" \
    https://toscrape.com
```

### Python client

```python
import asyncio

from zyte_api import AsyncZyteAPI


async def main():
    client = AsyncZyteAPI()
    api_response = await client.get(
        {
            "url": "https://toscrape.com",
            "browserHtml": True,
        }
    )
    print(api_response["browserHtml"])


asyncio.run(main())
```

### Scrapy

```python
from scrapy import Request, Spider


class ToScrapeSpider(Spider):
    name = "toscrape_com"

    async def start(self):
        yield Request(
            "https://toscrape.com",
            meta={
                "zyte_api_automap": {
                    "browserHtml": True,
                },
            },
        )

    def parse(self, response):
        browser_html: str = response.text
```

Output (first 5 lines):

```html
<!DOCTYPE html><html lang="en"><head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Scraping Sandbox</title>
        <link href="./css/bootstrap.min.css" rel="stylesheet">
        <link href="./css/main.css" rel="stylesheet">
```

## Screenshot

To get a webpage screenshot in browser requests, set the
[screenshot](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/screenshot) request field to `true` . The
[screenshot](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/response/200/screenshot) response field is the [Base64](https://en.wikipedia.org/wiki/Base64)-encoded screenshot
file data.

### Example

> [!NOTE]
> Install and configure [code example requirements](https://docs.pytest.org/en/stable/example/index.html#examples) and
> the [Zyte CA certificate](../../misc/ca.md#ca) to run the example below.

### CLI client

input.jsonl
```json
{"url": "https://toscrape.com", "screenshot": true}
```

```shell
zyte-api input.jsonl \
    | jq --raw-output .screenshot \
    | base64 --decode \
    > screenshot.jpg
```

### curl

input.json
```json
{
    "url": "https://toscrape.com",
    "screenshot": true
}
```

```shell
curl \
    --user YOUR_ZYTE_API_KEY: \
    --header 'Content-Type: application/json' \
    --data @input.json \
    --compressed \
    https://api.zyte.com/v1/extract \
    | jq --raw-output .screenshot \
    | base64 --decode \
    > screenshot.jpg
```

### Python client

```python
import asyncio
from base64 import b64decode

from zyte_api import AsyncZyteAPI


async def main():
    client = AsyncZyteAPI()
    api_response = await client.get(
        {
            "url": "https://toscrape.com",
            "screenshot": True,
        }
    )
    screenshot = b64decode(api_response["screenshot"])
    with open("screenshot.jpg", "wb") as f:
        f.write(screenshot)


asyncio.run(main())
```

### Scrapy

```python
from base64 import b64decode

from scrapy import Request, Spider


class ToScrapeComSpider(Spider):
    name = "toscrape_com"

    async def start(self):
        yield Request(
            "https://toscrape.com",
            meta={
                "zyte_api_automap": {
                    "screenshot": True,
                },
            },
        )

    def parse(self, response):
        screenshot: bytes = b64decode(response.raw_api_response["screenshot"])
```

Output:

![](zyte-api/usage/code-examples/output/screenshot.jpg)

## Actions

In browser requests use the [actions](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/actions) request field to define a
sequence of browser actions to perform before output generation.

> [!NOTE]
> ### See also
> 
> [Web scraping tutorial](../../web-scraping/tutorials/main/index.md#tutorial) ([Use an action sequence](../../web-scraping/tutorials/main/js.md#tutorial-actions)).

### Example: scrollBottom

> [!NOTE]
> Install and configure [code example requirements](https://docs.pytest.org/en/stable/example/index.html#examples) and
> the [Zyte CA certificate](../../misc/ca.md#ca) to run the example below.

### CLI client

input.jsonl
```json
{"url": "https://quotes.toscrape.com/scroll", "browserHtml": true, "actions": [{"action": "scrollBottom"}]}
```

```shell
zyte-api input.jsonl \
    | jq --raw-output .browserHtml \
    | xmllint --html --xpath 'count(//*[@class="quote"])' - 2> /dev/null
```

### curl

input.json
```json
{
    "url": "https://quotes.toscrape.com/scroll",
    "browserHtml": true,
    "actions": [
        {
            "action": "scrollBottom"
        }
    ]
}
```

```shell

curl \
    --user YOUR_ZYTE_API_KEY: \
    --header 'Content-Type: application/json' \
    --data @input.json \
    --compressed \
    https://api.zyte.com/v1/extract \
    | jq --raw-output .browserHtml \
    | xmllint --html --xpath 'count(//*[@class="quote"])' - 2> /dev/null
```

### Python client

```python
import asyncio

from parsel import Selector
from zyte_api import AsyncZyteAPI


async def main():
    client = AsyncZyteAPI()
    api_response = await client.get(
        {
            "url": "https://quotes.toscrape.com/scroll",
            "browserHtml": True,
            "actions": [
                {
                    "action": "scrollBottom",
                },
            ],
        },
    )
    browser_html = api_response["browserHtml"]
    quote_count = len(Selector(browser_html).css(".quote"))
    print(quote_count)


asyncio.run(main())
```

### Scrapy

```python
from scrapy import Request, Spider


class QuotesToScrapeComSpider(Spider):
    name = "quotes_toscrape_com"

    async def start(self):
        yield Request(
            "https://quotes.toscrape.com/scroll",
            meta={
                "zyte_api_automap": {
                    "browserHtml": True,
                    "actions": [
                        {
                            "action": "scrollBottom",
                        },
                    ],
                },
            },
        )

    def parse(self, response):
        quote_count = len(response.css(".quote"))
```

Output:

```none
100
```

### Example: Read from the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM)

> [!NOTE]
> Install and configure [code example requirements](https://docs.pytest.org/en/stable/example/index.html#examples) and
> the [Zyte CA certificate](../../misc/ca.md#ca) to run the example below.

To get content from the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM), use the `evaluate` action to create an
invisible DOM element, which you will get in [browserHtml](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/response/200/browserHtml), and
fill it with the desired content from the shadow DOM.

> [!TIP]
> If your `evaluate` action does not work as expected, check the
> [actions](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/response/200/actions) response field for errors.

The following example code shows how to access the shadow DOM paragraph from
[a shadow DOM example in CodePen](https://cdpn.io/TLadd/fullpage/PoGoQeV?anon=true&view=) using the
`evaluate` action with the following `source`:

```js
const div = document.createElement('div')
div.setAttribute('id', 'shadow-root-content')
// Hide, in case you also want to take a screenshot.
div.style.display = 'none'
const iframe = document.getElementById('result')
div.innerText = iframe
  .contentWindow.document
  .getElementById('shadow-root')
  .shadowRoot.querySelector('p').textContent
document.body.appendChild(div)
```

### Python

```python
import requests
from parsel import Selector

api_response = requests.post(
    "https://api.zyte.com/v1/extract",
    auth=("YOUR_ZYTE_API_KEY", ""),
    json={
        "url": "https://cdpn.io/TLadd/fullpage/PoGoQeV?anon=true&view=",
        "browserHtml": True,
        "actions": [
            {
                "action": "evaluate",
                "source": """
                    const div = document.createElement('div')
                    div.setAttribute('id', 'shadow-root-content')
                    div.style.display = 'none'
                    const iframe = document.getElementById('result')
                    div.innerText = iframe
                      .contentWindow.document
                      .getElementById('shadow-root')
                      .shadowRoot.querySelector('p').textContent
                    document.body.appendChild(div)
                """,
            },
        ],
    },
)
browser_html = api_response.json()["browserHtml"]
shadow_text = Selector(browser_html).css("#shadow-root-content::text").get()
print(shadow_text)
```

### Python client

```python
import asyncio

from parsel import Selector
from zyte_api import AsyncZyteAPI


async def main():
    client = AsyncZyteAPI()
    api_response = await client.get(
        {
            "url": "https://cdpn.io/TLadd/fullpage/PoGoQeV?anon=true&view=",
            "browserHtml": True,
            "actions": [
                {
                    "action": "evaluate",
                    "source": """
                        const div = document.createElement('div')
                        div.setAttribute('id', 'shadow-root-content')
                        div.style.display = 'none'
                        const iframe = document.getElementById('result')
                        div.innerText = iframe
                          .contentWindow.document
                          .getElementById('shadow-root')
                          .shadowRoot.querySelector('p').textContent
                        document.body.appendChild(div)
                    """,
                },
            ],
        },
    )
    browser_html = api_response["browserHtml"]
    shadow_text = Selector(browser_html).css("#shadow-root-content::text").get()
    print(shadow_text)


asyncio.run(main())
```

### Scrapy

```python
from scrapy import Request, Spider


class CodePenSpider(Spider):
    name = "codepen"

    async def start(self):
        yield Request(
            "https://cdpn.io/TLadd/fullpage/PoGoQeV?anon=true&view=",
            meta={
                "zyte_api_automap": {
                    "browserHtml": True,
                    "actions": [
                        {
                            "action": "evaluate",
                            "source": """
                                const div = document.createElement('div')
                                div.setAttribute('id', 'shadow-root-content')
                                div.style.display = 'none'
                                const iframe = document.getElementById('result')
                                div.innerText = iframe
                                  .contentWindow.document
                                  .getElementById('shadow-root')
                                  .shadowRoot.querySelector('p').textContent
                                document.body.appendChild(div)
                            """,
                        },
                    ],
                },
            },
        )

    def parse(self, response):
        shadow_text = response.css("#shadow-root-content::text").get()
        print(shadow_text)
```

Output:

```none
Shadow Paragraph
```

### Action types

Zyte API supports 3 types of browser actions:

- **Generic actions** work on every website. They allow you to type text into
  input fields, emulate mouse input, and wait for events or time.

- **Special actions** expose functionality that requires specific knowledge
  of the target website, such as using their search box or filling a form.

  They are only available for certain websites. To find out if an action is
  available for a given website, send a test request using that action. If
  the action is not supported, you will get an error API response indicating
  so.
- [Browser scripts](../ide/index.md#zapi-scripts).

### Action limits

You are free to use as many browser actions as you wish, but total browser
execution time is limited to 60 seconds. If your actions are still running by
that time, the on-going action is interrupted, follow-up actions are not
executed at all, and you get your requested output ([browser HTML](#zapi-browser-html), [screenshot](#zapi-screenshot)) as it was rendered
at that time.

The Zyte API response includes an `action` key that provides details about
action execution, including `elapsedTime`, `error`, and `status` fields
to help you debug your actions, e.g. to find out which actions were executed
successfully and which actions were not.

> [!TIP]
> For workflows that need more than 60 seconds, use [CDP](cdp.md#cdp)
> instead, where session time is limited by `ttl` up to 3600 seconds.

### Action selectors

Browser actions that interact with a webpage element all have a `selector`
key that allows you to define how to find the target webpage element.

You must define a query to find the target webpage element in the
`selector.value` field.

You must specify the language of your query in the `selector.type` field,
which supports the following values: CSS Selector (`css`), XPath 1.0
(`xpath`). For information about these query languages, see [Learning CSS and
XPath](https://parsel.readthedocs.io/en/latest/usage.html#learning-css-and-xpath).

Note that selectors cannot interact with [iframes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) or with the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM),
only the [evaluate action](#zapi-actions), [browser scripts](../ide/index.md#zapi-scripts), and [CDP](cdp.md#cdp) can.

### Wait actions

You can use the following browser actions to introduce wait times in your
browser action sequences or in your [browser scripts](../ide/index.md#zapi-scripts):
`waitForSelector`, `waitForRequest`, `waitForResponse`, and
`waitForTimeout`.

Whenever you need to wait for something to happen on a webpage, your should
consider using `waitForSelector` first. It waits for an element matching a
given [selector](#zapi-selector). By default, it waits for a matching
*visible* element, but you can change `selector.state` to `attached`, to
wait for an element to exist regardless of visibility, or to `hidden`, to
wait for a matching *invisible* element.

> [!TIP]
> For a usage example of `waitForSelector`, see the [web scraping
> tutorial](../../web-scraping/tutorials/main/js.md#tutorial-actions).

`waitForRequest` and `waitForResponse` wait for a request to be sent or for
a response to be received, filtering by URL pattern.

`waitForTimeout` pauses your sequence of actions or your browser script for
the specified amount of time. Because [action run time is limited](#zapi-action-limits), you should avoid using this type of action when an
alternative waiting action can replace it. However, this action can be
necessary for certain scenarios, such as following organic website-access
patterns.

## Network capture

In browser requests, use the [networkCapture](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/networkCapture) request field to
define filters to capture network responses received during browser rendering
(including [action execution](#zapi-actions)).

### Example

> [!NOTE]
> Install and configure [code example requirements](https://docs.pytest.org/en/stable/example/index.html#examples) and
> the [Zyte CA certificate](../../misc/ca.md#ca) to run the example below.

### CLI client

input.jsonl
```json
{"url": "https://quotes.toscrape.com/scroll", "browserHtml": true, "networkCapture": [{"filterType": "url", "httpResponseBody": true, "value": "/api/", "matchType": "contains"}]}
```

```shell
zyte-api input.jsonl \
    | jq --raw-output ".networkCapture[0].httpResponseBody" \
    | base64 --decode \
    | jq --raw-output ".quotes[0].author.name"
```

### curl

input.json
```json
{
    "url": "https://quotes.toscrape.com/scroll",
    "browserHtml": true,
    "networkCapture": [
        {
            "filterType": "url",
            "httpResponseBody": true,
            "value": "/api/",
            "matchType": "contains"
        }
    ]
}
```

```shell
curl \
    --user YOUR_ZYTE_API_KEY: \
    --header 'Content-Type: application/json' \
    --data @input.json \
    --compressed \
    https://api.zyte.com/v1/extract \
    | jq --raw-output ".networkCapture[0].httpResponseBody" \
    | base64 --decode \
    | jq --raw-output ".quotes[0].author.name"
```

### Python client

```python
import asyncio
import json
from base64 import b64decode

from zyte_api import AsyncZyteAPI


async def main():
    client = AsyncZyteAPI()
    api_response = await client.get(
        {
            "url": "https://quotes.toscrape.com/scroll",
            "browserHtml": True,
            "networkCapture": [
                {
                    "filterType": "url",
                    "httpResponseBody": True,
                    "value": "/api/",
                    "matchType": "contains",
                },
            ],
        },
    )
    capture = api_response["networkCapture"][0]
    data = json.loads(b64decode(capture["httpResponseBody"]).decode())
    print(data["quotes"][0]["author"]["name"])


asyncio.run(main())
```

### Scrapy

```python
import json
from base64 import b64decode

from scrapy import Request, Spider


class QuotesToScrapeComSpider(Spider):
    name = "quotes_toscrape_com"

    async def start(self):
        yield Request(
            "https://quotes.toscrape.com/scroll",
            meta={
                "zyte_api_automap": {
                    "browserHtml": True,
                    "networkCapture": [
                        {
                            "filterType": "url",
                            "httpResponseBody": True,
                            "value": "/api/",
                            "matchType": "contains",
                        },
                    ],
                },
            },
        )

    def parse(self, response):
        capture = response.raw_api_response["networkCapture"][0]
        data = json.loads(b64decode(capture["httpResponseBody"]).decode())
        print(data["quotes"][0]["author"]["name"])
```

Output:

```none
Albert Einstein
```

See also [Use network capture](../../web-scraping/tutorials/main/js.md#tutorial-network-capture) in the [web scraping tutorial](../../web-scraping/tutorials/main/index.md#tutorial).

## Request headers

In browser requests, use the [requestHeaders.referer](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/requestHeaders.referer) request
field to set the [Referer header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer).

### Example

> [!NOTE]
> Install and configure [code example requirements](https://docs.pytest.org/en/stable/example/index.html#examples) and
> the [Zyte CA certificate](../../misc/ca.md#ca) to run the example below.

### CLI client

input.jsonl
```json
{"url": "https://httpbin.org/anything", "browserHtml": true, "requestHeaders": {"referer": "https://example.org/"}}
```

```shell
zyte-api input.jsonl \
    | jq --raw-output .browserHtml \
    | xmllint --html --xpath '//text()' - 2> /dev/null \
    | jq .headers
```

### curl

input.json
```json
{
    "url": "https://httpbin.org/anything",
    "browserHtml": true,
    "requestHeaders": {
        "referer": "https://example.org/"
    }
}
```

```shell
curl \
    --user YOUR_ZYTE_API_KEY: \
    --header 'Content-Type: application/json' \
    --data @input.json \
    --compressed \
    https://api.zyte.com/v1/extract \
    | jq --raw-output .browserHtml \
    | xmllint --html --xpath '//text()' - 2> /dev/null \
    | jq .headers
```

### Python client

```python
import asyncio
import json

from parsel import Selector
from zyte_api import AsyncZyteAPI


async def main():
    client = AsyncZyteAPI()
    api_response = await client.get(
        {
            "url": "https://httpbin.org/anything",
            "browserHtml": True,
            "requestHeaders": {
                "referer": "https://example.org/",
            },
        }
    )
    browser_html = api_response["browserHtml"]
    selector = Selector(browser_html)
    response_json = selector.xpath("//text()").get()
    response_data = json.loads(response_json)
    print(json.dumps(response_data["headers"], indent=2))


asyncio.run(main())
```

### Scrapy

```python
import json

from scrapy import Request, Spider


class HTTPBinOrgSpider(Spider):
    name = "httpbin_org"

    async def start(self):
        yield Request(
            "https://httpbin.org/anything",
            headers={"Referer": "https://example.org/"},
            meta={
                "zyte_api_automap": {
                    "browserHtml": True,
                },
            },
        )

    def parse(self, response):
        response_json = response.xpath("//text()").get()
        response_data = json.loads(response_json)
        headers = response_data["headers"]
```

Output (`"Referer"` line):

```json
  "Referer": "https://example.org/",
```

At the moment, only the [Referer header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) can be overridden this way. If you
need to override additional headers, use [HTTP requests](http.md#zapi-http) with their [customHttpRequestHeaders](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/customHttpRequestHeaders) request
field instead, or use [CDP](cdp.md#cdp) for arbitrary request headers on a
rendered page.

## Redirection

Browser requests always follow [HTTP redirection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections) and other URL changes
triggered during browser rendering, e.g. by HTML or by JavaScript.

> [!TIP]
> [HTTP requests support not following redirection](http.md#zapi-http-redirection).

## JavaScript

Browser requests have JavaScript execution enabled by default for most
websites. For some websites, however, JavaScript execution is disabled by
default because it helps avoiding bans or automating extraction.

Use the [javascript](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/javascript) request field to force whether or not
JavaScript execution should be enabled on a browser request.

### Example

> [!NOTE]
> Install and configure [code example requirements](https://docs.pytest.org/en/stable/example/index.html#examples) and
> the [Zyte CA certificate](../../misc/ca.md#ca) to run the example below.

### CLI client

input.jsonl
```json
{"url": "https://www.whatismybrowser.com/detect/is-javascript-enabled", "browserHtml": true, "javascript": false}
```

```shell
zyte-api input.jsonl \
    | jq --raw-output .browserHtml \
    | xmllint --html --xpath '//*[@id="detected_value"]/text()' - 2> /dev/null
```

### curl

input.json
```json
{
    "url": "https://www.whatismybrowser.com/detect/is-javascript-enabled",
    "browserHtml": true,
    "javascript": false
}
```

```shell
curl \
    --user YOUR_ZYTE_API_KEY: \
    --header 'Content-Type: application/json' \
    --data @input.json \
    --compressed \
    https://api.zyte.com/v1/extract \
    | jq --raw-output .browserHtml \
    | xmllint --html --xpath '//*[@id="detected_value"]/text()' - 2> /dev/null
```

### Python client

```python
import asyncio

from parsel import Selector
from zyte_api import AsyncZyteAPI


async def main():
    client = AsyncZyteAPI()
    api_response = await client.get(
        {
            "url": "https://www.whatismybrowser.com/detect/is-javascript-enabled",
            "browserHtml": True,
            "javascript": False,
        }
    )
    browser_html = api_response["browserHtml"]
    selector = Selector(browser_html)
    is_javascript_enabled = selector.css("#detected_value::text").get()
    print(is_javascript_enabled)


asyncio.run(main())
```

### Scrapy

```python
from scrapy import Request, Spider


class WhatIsMyBrowserComSpider(Spider):
    name = "whatismybrowser_com"

    async def start(self):
        yield Request(
            "https://www.whatismybrowser.com/detect/is-javascript-enabled",
            meta={
                "zyte_api_automap": {
                    "browserHtml": True,
                    "javascript": False,
                },
            },
        )

    def parse(self, response):
        is_javascript_enabled: str = response.css("#detected_value::text").get()
```

Output:

```none
No
```
