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).
- 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:
TestHarnessfor 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
| Crate | Description | Docs |
|---|---|---|
viewpoint-cdp |
Low-level Chrome DevTools Protocol client | |
viewpoint-core |
High-level browser automation API | |
viewpoint-test |
Test framework with assertions and fixtures | |
viewpoint-test-macros |
Proc macros for convenient test setup | |
viewpoint-js |
JavaScript interpolation macro |
Add to your Cargo.toml:
[dev-dependencies]
viewpoint-test = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }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
}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(())
}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();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?;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?;Configure test behavior with the builder:
let harness = TestHarness::builder()
.headless(false) // Show browser window
.timeout(Duration::from_secs(60))
.build()
.await?;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?;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(())
}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(())
}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(())
}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?;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(())
}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(())
}- Rust 1.85+
- Chromium browser (automatically found via
CHROMIUM_PATHor system PATH)
# 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_testTests are organized following the code-quality spec:
- Unit tests: Located in
src/<module>/tests/mod.rswithin 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.
MIT