Skip to content

stephenstubbs/viewpoint

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Viewpoint

Crates.io Documentation License: MIT

A Rust-native browser automation library inspired by Playwright. Viewpoint provides a high-level API for controlling Chromium browsers via the Chrome DevTools Protocol (CDP).

Features

  • Browser Automation: Launch and control Chromium browsers programmatically
  • Page Navigation: Navigate to URLs with configurable wait states
  • Element Locators: Find elements using CSS selectors, text, ARIA roles, labels, and more
  • Actions: Click, fill, type, hover, check/uncheck, select options
  • Assertions: Fluent async assertions for testing element and page state
  • Test Framework: TestHarness for easy test setup with automatic cleanup
  • Network Interception: Route, mock, and HAR replay for network requests
  • Event System: Handle popups, dialogs, downloads, console, and page errors
  • WebSocket Monitoring: Observe WebSocket connections and messages
  • Exposed Functions: Call Rust functions from JavaScript
  • Accessibility Testing: ARIA snapshots for accessibility verification
  • Soft Assertions: Collect multiple failures without stopping test execution
  • HTTP Auth: Automatic handling of HTTP Basic/Digest authentication
  • Tracing: Record traces for debugging test failures

Crates

Crate Description Docs
viewpoint-cdp Low-level Chrome DevTools Protocol client docs.rs
viewpoint-core High-level browser automation API docs.rs
viewpoint-test Test framework with assertions and fixtures docs.rs
viewpoint-test-macros Proc macros for convenient test setup docs.rs
viewpoint-js JavaScript interpolation macro docs.rs

Quick Start

Add to your Cargo.toml:

[dev-dependencies]
viewpoint-test = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Basic Test with TestHarness

use viewpoint_test::{expect, expect_page, TestHarness, DocumentLoadState};

#[tokio::test]
async fn my_test() -> Result<(), Box<dyn std::error::Error>> {
    // Create a test harness - launches browser, creates context and page
    let harness = TestHarness::new().await?;
    let page = harness.page();

    // Navigate to a URL
    page.goto("https://example.com")
        .wait_until(DocumentLoadState::DomContentLoaded)
        .goto()
        .await?;

    // Verify page state
    expect_page(page).to_have_title("Example Domain").await?;

    // Find and verify elements
    let heading = page.locator("h1");
    expect(&heading).to_be_visible().await?;
    expect(&heading).to_have_text("Example Domain").await?;

    Ok(()) // harness automatically cleans up on drop
}

Using the Test Macro

use viewpoint_test::{test, Page, expect};

#[viewpoint_test::test]
async fn my_macro_test(page: &Page) -> Result<(), Box<dyn std::error::Error>> {
    page.goto("https://example.com").goto().await?;
    
    let heading = page.locator("h1");
    expect(&heading).to_be_visible().await?;
    
    Ok(())
}

Locators

Viewpoint provides multiple ways to locate elements:

// CSS selector
let button = page.locator("button.submit");

// By text content
let heading = page.get_by_text("Welcome");

// By ARIA role
let nav = page.get_by_role(AriaRole::Navigation).build();

// By test ID (data-testid attribute)
let form = page.get_by_test_id("login-form");

// By label (for form controls)
let email = page.get_by_label("Email address");

// By placeholder
let search = page.get_by_placeholder("Search...");

// Chaining
let item = page.locator("ul").locator("li").first();

Actions

Perform actions on located elements:

// Click
button.click().await?;
button.dblclick().await?;

// Form input
input.fill("Hello World").await?;
input.type_text("typing...").await?;  // Character by character
input.clear().await?;

// Keyboard
input.press("Enter").await?;
input.press("Control+a").await?;

// Checkbox/Radio
checkbox.check().await?;
checkbox.uncheck().await?;

// Dropdown
select.select_option("value").await?;
select.select_options(&["a", "b", "c"]).await?;  // Multi-select

// Mouse
element.hover().await?;
element.focus().await?;

Assertions

Fluent async assertions with auto-waiting:

use viewpoint_test::{expect, expect_page};

// Element assertions
expect(&locator).to_be_visible().await?;
expect(&locator).to_be_hidden().await?;
expect(&locator).to_have_text("Hello").await?;
expect(&locator).to_contain_text("ello").await?;
expect(&locator).to_have_attribute("href", "/path").await?;
expect(&locator).to_have_class("active").await?;
expect(&locator).to_be_enabled().await?;
expect(&locator).to_be_disabled().await?;
expect(&locator).to_be_checked().await?;

// Page assertions
expect_page(page).to_have_url("https://example.com").await?;
expect_page(page).to_have_url_containing("/path").await?;
expect_page(page).to_have_title("Page Title").await?;

// Negation
expect(&locator).not().to_be_visible().await?;

// Custom timeout
expect(&locator)
    .timeout(Duration::from_secs(10))
    .to_be_visible()
    .await?;

Test Configuration

Configure test behavior with the builder:

let harness = TestHarness::builder()
    .headless(false)  // Show browser window
    .timeout(Duration::from_secs(60))
    .build()
    .await?;

Fixture Scoping

Share browser/context across tests for faster execution:

// Test-scoped (default): New browser per test
let harness = TestHarness::new().await?;

// Module-scoped: Share browser, fresh context per test
let harness = TestHarness::from_browser(&shared_browser).await?;

// Shared context: Share context, fresh page per test
let harness = TestHarness::from_context(&shared_context).await?;

Popup Handling

Handle popups and new windows triggered by page actions:

use viewpoint_test::{TestHarness, expect_page};

#[tokio::test]
async fn test_popup() -> Result<(), Box<dyn std::error::Error>> {
    let harness = TestHarness::new().await?;
    let page = harness.page();
    let context = harness.context();
    
    page.goto("https://example.com").goto().await?;
    
    // Wait for popup triggered by clicking a link
    let popup = context.wait_for_page(|| async {
        page.locator("a[target=_blank]").click().await?;
        Ok(())
    }).await?;
    
    // Work with the popup page
    expect_page(&popup).to_have_url_containing("/new-page").await?;
    popup.locator("button").click().await?;
    
    Ok(())
}

Soft Assertions

Collect multiple assertion failures without stopping test execution:

use viewpoint_test::SoftAssertions;

#[tokio::test]
async fn test_with_soft_assertions() -> Result<(), Box<dyn std::error::Error>> {
    let harness = TestHarness::new().await?;
    let page = harness.page();
    
    page.goto("https://example.com").goto().await?;
    
    let soft = SoftAssertions::new();
    
    // These don't stop on failure
    soft.expect(&page.locator("h1")).to_have_text("Title").await;
    soft.expect(&page.locator(".content")).to_be_visible().await;
    
    // Assert all passed (reports all failures at once)
    soft.assert_all()?;
    
    Ok(())
}

ARIA Accessibility Testing

Verify accessibility tree structure:

use viewpoint_test::{expect, TestHarness};

#[tokio::test]
async fn test_accessibility() -> Result<(), Box<dyn std::error::Error>> {
    let harness = TestHarness::new().await?;
    let page = harness.page();
    
    page.goto("https://example.com").goto().await?;
    
    // Assert expected ARIA structure
    expect(&page.locator("nav")).to_match_aria_snapshot(r#"
      - navigation:
        - link "Home"
        - link "About"
    "#).await?;
    
    Ok(())
}

Network Interception

Mock network requests or serve responses from HAR files:

use viewpoint_test::{TestHarness, Route};

#[tokio::test]
async fn test_network_mock() -> Result<(), Box<dyn std::error::Error>> {
    let harness = TestHarness::new().await?;
    let page = harness.page();
    
    // Mock an API endpoint
    page.route("**/api/users", |route: Route| async move {
        route.fulfill()
            .status(200)
            .json(&serde_json::json!({"users": []}))
            .send()
            .await
    }).await?;
    
    page.goto("https://example.com").goto().await?;
    
    Ok(())
}

// HAR replay - serve responses from recorded HAR file
page.route_from_har("recordings/api.har").await?;

WebSocket Monitoring

Observe WebSocket connections and messages:

use viewpoint_test::TestHarness;

#[tokio::test]
async fn test_websocket() -> Result<(), Box<dyn std::error::Error>> {
    let harness = TestHarness::new().await?;
    let page = harness.page();
    
    page.on_websocket(|ws| async move {
        println!("WebSocket opened: {}", ws.url());
        
        ws.on_framereceived(|frame| async move {
            println!("Received: {:?}", frame.payload());
            Ok(())
        }).await?;
        
        Ok(())
    }).await?;
    
    page.goto("https://example.com").goto().await?;
    
    Ok(())
}

Exposed Functions

Call Rust functions from JavaScript:

use viewpoint_test::TestHarness;

#[tokio::test]
async fn test_exposed_function() -> Result<(), Box<dyn std::error::Error>> {
    let harness = TestHarness::new().await?;
    let page = harness.page();
    
    // Expose a Rust function to JavaScript
    page.expose_function("compute", |args| async move {
        let x = args[0].as_i64().unwrap_or(0);
        let y = args[1].as_i64().unwrap_or(0);
        Ok(serde_json::json!(x + y))
    }).await?;
    
    page.goto("https://example.com").goto().await?;
    
    // JavaScript can call: await window.compute(1, 2)
    let result: i64 = page.evaluate("await window.compute(1, 2)").await?;
    assert_eq!(result, 3);
    
    Ok(())
}

Requirements

  • Rust 1.85+
  • Chromium browser (automatically found via CHROMIUM_PATH or system PATH)

Development

Running Tests

# Run unit tests only (no browser required)
cargo test --workspace

# Run all tests including browser integration tests
cargo test --workspace --features integration

# Run with visible browser (integration tests only)
HEADLESS=false cargo test --workspace --features integration

# Run specific test file
cargo test -p viewpoint-test --test harness_tests --features integration

# Run specific crate's tests
cargo test -p viewpoint-js        # js! macro tests
cargo test -p viewpoint-cdp       # CDP protocol tests
cargo test -p viewpoint-core      # Browser automation tests
cargo test -p viewpoint-test      # Test framework tests

# Run examples
cargo run -p viewpoint-test --example basic_test

Test Organization

Tests are organized following the code-quality spec:

  • Unit tests: Located in src/<module>/tests/mod.rs within each crate
  • Integration tests: Located in crates/<crate>/tests/*.rs, feature-gated with #![cfg(feature = "integration")]
  • Doc tests: Inline in source files, run with cargo test --doc

All modules use the folder structure (module/mod.rs) with tests in external directories.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages