Astral Browser Automation

repository·main·Indexed 43 days ago

https://github.com/lino-levan/astral

A high-level browser automation library for Deno providing a Puppeteer/Playwright-like API. It utilizes the Chrome DevTools Protocol (CDP) and a specialized binding system called Celestial to control Chromium-based browsers. Features include support for the RAII pattern via the `using` keyword, remote browser connection via `connect()`, HTTP request interception for mocking, and integrated code coverage for `page.evaluate()`.

Tokens
15.7K
Snippets
58
Records
72
Agent score
52%

What's inside Astral

  1. Overview of Astral Bindings and Celestial

    main
    Astral provides bindings to the Chrome DevTools Protocol (CDP). Because the CDP is extremely large and changes frequently, Astral uses a specialized system called Celestial to manage these protocol bindings. Celestial is a home-grown implementation designed to ensure that the bindings remain up-to-date with the evolving CDP specification, preventing the manual overhead and staleness associated with hand-written bindings.
  2. Astral API Design Philosophy

    main
    Astral's API is designed to follow the patterns established by puppeteer and playwright, as these are widely recognized and popular in the browser automation space. However, Astral aims to improve upon specific design choices found in those libraries, such as reducing ambiguity in top-level methods (e.g., avoiding overlapping type and click methods that both accept selectors) and simplifying the complexity of the selectors API.
  3. Understand the core components of Astral

    main

    Astral is composed of three primary layers that work together:

    1. Browser Binaries: A Chrome or Firefox binary that is executed to perform tasks.
    2. Bindings: Low-level bindings generated specifically for those browser binaries.
    3. API Layer: A high-level abstraction layer built on top of the bindings to provide a developer-friendly API.

    Understanding this hierarchy is essential for understanding how Astral interacts with the browser and how the high-level commands translate into low-level browser actions.

  4. How Astral generates CDP bindings

    main

    The binding generation process is managed by a script located in bindings/_tools/mod.ts. The generation workflow follows three main steps:

    1. Protocol Extraction: The script fetches the protocol JSON directly from a running browser instance at http://localhost:9222/json/protocol.
    2. Type Extraction: It parses the retrieved JSON to extract the necessary type definitions.
    3. Binding Generation: It generates bindings for both methods and events using specific format strings.

    After the generation is complete, deno fmt is automatically run to ensure the generated code is properly formatted.

  5. Astral Implementation Details and CDP

    main
    Astral functions as a high-level wrapper around celestial bindings. Many of the APIs are implemented by mapping directly to the Chrome DevTools Protocol (CDP) API, similar to how Puppeteer operates. For APIs that do not map directly to CDP—such as the waitFor* family of functions—the implementation involves more complex logic to bridge the gap between the high-level API and the underlying protocol.
  6. Understand Astral's CDP compatibility

    main

    Astral interfaces with browsers using the Chrome DevTools Protocol (CDP). This means any browser that implements the CDP protocol should be compatible with Astral out of the box.

    Supported Browsers:

    • Browsers implementing CDP (e.g., Chromium-based browsers).
    • Firefox (implementation status: likely compatible, but untested).
    • Webkit-backed browsers (currently not supported).

    Communication Method: Astral currently interacts with the protocol via an exposed port and a websocket rather than a unix pipe.

  7. Quickstart: Launch a browser and take a screenshot with Astral

    main

    Astral is a Puppeteer/Playwright-like library for Deno. You can use it to automate browser tasks such as generating screenshots/PDFs, crawling SPAs for pre-rendering, and automating UI testing. To get started, import launch from jsr:@astral/astral, use browser.newPage(url) to open a page, and page.screenshot() to capture the view.

    Note: Astral is designed to be stealthy by default without requiring extra configuration.

    // Import Astral
    import { launch } from "jsr:@astral/astral";
    
    // Launch the browser
    const browser = await launch();
    
    // Open a new page
    const page = await browser.newPage("https://deno.land");
    
    // Take a screenshot of the page and save that to disk
    const screenshot = await page.screenshot();
    Deno.writeFileSync("screenshot.png", screenshot);
    
    // Close the browser
    await browser.close();
  8. Use the HTTP interceptor to mock or fail requests

    main

    The interceptor function allows you to capture outgoing requests and either provide a custom Response (mocking) or throw an InterceptorError (simulating failure). This is useful for testing with fixtures or simulating network errors without making actual network calls.

    Control Flow

    The behavior of an intercepted request is determined by the return value of the interceptor handler:

    • Continue Request: Return null, undefined, or a void value to let the request proceed normally.
    • Fulfill with Mock: Return a Response object to fulfill the request with that specific response.
    • Fail Request: Throw an InterceptorError to fail the request with a specific reason.

    Filtering by Resource Type

    The interceptor receives a Request object and an info record containing resourceType: Network_ResourceType. You can use this to filter logic based on whether the request is a Document, Stylesheet, Image, etc.

    Important Security Note

    The HTTP interceptor executes before sandbox permission checks. This means intercepted requests are not subject to the sandbox's permission restrictions.

    await using browser = await launch();
    await using page = await browser.newPage("http://example.com", {
      interceptor(request: Request) {
        if (new URL(request.url).pathname === "/blocked") {
          throw new InterceptorError("BlockedByClient");
        }
    
        return new Response(
          "<!DOCTYPE html><html><head><title>Intercepted request!</title></head><body></body></html>",
          { status: 200, headers: { "content-type": "text/html" } },
        );
      },
    });
  9. Perform complex navigation in Astral

    main

    To automate complex user flows like searching and navigating through links, use the launch function to start a browser, newPage to open a URL, and standard selector methods ($) to interact with elements. When clicking links that trigger page loads, wrap the click action in Promise.all with page.waitForNavigation() to ensure the script waits for the new page to load before proceeding.

    // Import Astral
    import { launch } from "jsr:@astral/astral";
    
    // Launch browser in headfull mode
    const browser = await launch({ headless: false });
    
    // Open the webpage
    const page = await browser.newPage("https://deno.land");
    
    // Click the search button
    const button = await page.$("button");
    await button!.click();
    
    // Type in the search input
    const input = await page.$("#search-input");
    await input!.type("pyro", { delay: 1000 });
    
    // Wait for the search results to come back
    await page.waitForNetworkIdle({ idleConnections: 0, idleTime: 1000 });
    
    // Click the 'pyro' link
    const xLink = await page.$("a.justify-between:nth-child(1)");
    await Promise.all([
      page.waitForNavigation(),
      xLink!.click(),
    ]);
    
    // Click the link to 'pyro.deno.dev'
    const dLink = await page.$(
      ".markdown-body > p:nth-child(8) > a:nth-child(1)",
    );
    await Promise.all([
      page.waitForNavigation(),
      dLink!.click(),
    ]);
    
    // Close browser
    await browser.close();
  10. Take screenshots of web pages with Astral

    main

    You can capture screenshots of web pages by using the launch() function to start a browser instance, navigating to a URL with browser.newPage(), and calling page.screenshot(). The screenshot() method returns the image data as a buffer (typically a Uint8Array), which you can then write to disk using your runtime's file system API (e.g., Deno.writeFileSync in Deno).

    // Import Astral
    import { launch } from "jsr:@astral/astral";
    
    // Launch the browser
    const browser = await launch();
    
    // Open a new page
    const page = await browser.newPage("https://deno.land");
    
    // Take a screenshot of the page and save that to disk
    const screenshot = await page.screenshot();
    Deno.writeFileSync("screenshot.png", screenshot);
    
    // Close the browser
    await browser.close();
  11. Connect to a remote browser endpoint

    main

    Instead of spawning a new local browser process, you can connect astral to an existing browser process or a remote browser service (like browserless.io) by providing a WebSocket URL to the endpoint option in the launch function. This is useful for using remote automation services or connecting to a browser already running in a different environment.

    // Import Astral
    import { launch } from "jsr:@astral/astral";
    
    // Connect to remote endpoint
    const browser = await launch({
      endpoint: "wss://remote-browser-endpoint.example.com",
    });
    
    // Do stuff
    const page = await browser.newPage("http://example.com");
    console.log(await page.evaluate(() => document.title));
    
    // Close connection
    await browser.close();