Skip to content

Commit ec7a9c9

Browse files
committed
feat(browser): add browser automation support
- Introduce a new DSL configuration (BrowserTestConfig) for defining browser actions - Implement BrowserTestItem conversion and browser action execution using chromiumoxide - Update request processing to route tests with a browser configuration to the new browser automation flow - Provide example YAML configuration for browser tests (e.g. browser_test.tk.yaml) - Adjust default handling for HTTP method flattening to accommodate browser tests This commit lays the foundation for automating browser interactions as part of the test suite.
1 parent c7a478f commit ec7a9c9

8 files changed

+363
-362
lines changed

Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ serde_with = "3.0.0"
3434
colored_json = "5"
3535
chrono = "0.4.26"
3636
walkdir = "2.3.3"
37-
# core-foundation = {git="https://github.com/servo/core-foundation-rs", rev="9effb788767458ad639ce36229cc07fd3b1dc7ba"}
37+
chromiumoxide = "0.7"
38+
futures-util = "0.3"
3839

40+
# core-foundation = {git="https://github.com/servo/core-foundation-rs", rev="9effb788767458ad639ce36229cc07fd3b1dc7ba"}
3941
[dev-dependencies]
4042
httpmock = "0.7"
4143
testing_logger = "0.1.1"

browser_test.tk.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- title: "Browser: Open Google"
2+
GET: "" # This key satisfies the flattened HttpMethod (default is GET)
3+
browser:
4+
action: "navigate"
5+
url: "https://www.google.com"
File renamed without changes.

src/base_browser.rs

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// src/base_browser.rs
2+
3+
use anyhow::Result;
4+
use chromiumoxide::browser::{Browser, BrowserConfig, HeadlessMode};
5+
use chromiumoxide::page::ScreenshotParams;
6+
use futures_util::stream::StreamExt;
7+
use serde::{Deserialize, Serialize};
8+
use std::collections::HashMap;
9+
10+
// Define the supported browser actions.
11+
#[derive(Debug, Serialize, Deserialize)]
12+
pub enum BrowserAction {
13+
/// Navigate to a URL.
14+
Navigate { url: String },
15+
/// Click an element specified by a CSS selector.
16+
Click { selector: String },
17+
/// Fill out form fields and then click the submit element.
18+
FillAndSubmit {
19+
fields: HashMap<String, String>,
20+
submit_selector: String,
21+
},
22+
}
23+
24+
// A browser test item combines an optional title with a browser action.
25+
#[derive(Debug, Serialize, Deserialize)]
26+
pub struct BrowserTestItem {
27+
pub title: Option<String>,
28+
pub browser: BrowserAction,
29+
}
30+
31+
// Import the DSL browser configuration from base_request.
32+
use crate::base_request::BrowserTestConfig;
33+
34+
impl BrowserTestItem {
35+
/// Converts a DSL browser configuration (BrowserTestConfig) into a BrowserTestItem.
36+
pub fn from_config(title: Option<String>, config: &BrowserTestConfig) -> Self {
37+
let action = match config.action.as_str() {
38+
"navigate" => BrowserAction::Navigate {
39+
url: config.url.clone().unwrap_or_default(),
40+
},
41+
"click" => BrowserAction::Click {
42+
selector: config.selector.clone().unwrap_or_default(),
43+
},
44+
"fillAndSubmit" => BrowserAction::FillAndSubmit {
45+
fields: config.fields.clone().unwrap_or_default(),
46+
submit_selector: config.submit_selector.clone().unwrap_or_default(),
47+
},
48+
_ => BrowserAction::Navigate { url: "".into() },
49+
};
50+
BrowserTestItem { title, browser: action }
51+
}
52+
}
53+
54+
/// Runs the browser action defined by the BrowserTestItem.
55+
pub async fn run_browser_action(item: BrowserTestItem) -> Result<()> {
56+
log::info!("Running browser test: {:?}", item);
57+
58+
let (mut browser, mut handler) = Browser::launch(
59+
BrowserConfig::builder()
60+
.headless_mode(HeadlessMode::True)
61+
.build()
62+
.map_err(|e| anyhow::anyhow!(e))?
63+
)
64+
.await?;
65+
66+
// Process browser events in a spawned task.
67+
tokio::spawn(async move {
68+
while let Some(event) = handler.next().await {
69+
log::debug!("Browser event: {:?}", event);
70+
}
71+
});
72+
73+
let page = browser.new_page("about:blank").await?;
74+
75+
match item.browser {
76+
BrowserAction::Navigate { url } => {
77+
page.goto(&url).await?;
78+
page.wait_for_navigation().await?;
79+
}
80+
BrowserAction::Click { selector } => {
81+
let element = page.find_element(&selector).await?;
82+
element.click().await?;
83+
}
84+
BrowserAction::FillAndSubmit { fields, submit_selector } => {
85+
for (field_selector, value) in fields {
86+
let element = page.find_element(&field_selector).await?;
87+
element.type_str(&value).await?;
88+
}
89+
let submit_element = page.find_element(&submit_selector).await?;
90+
submit_element.click().await?;
91+
page.wait_for_navigation().await?;
92+
}
93+
}
94+
95+
let screenshot = page.screenshot(ScreenshotParams::default()).await?;
96+
log::info!("Screenshot taken ({} bytes)", screenshot.len());
97+
98+
page.close().await?;
99+
browser.close().await?;
100+
Ok(())
101+
}

src/base_cli.rs

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
// src/base_cli.rs
2+
13
use clap::{Parser, Subcommand};
24
use std::path::PathBuf;
35

46
#[derive(Parser)]
5-
#[command(name = "testkit")]
6-
#[command(author = "APIToolkit. <[email protected]>")]
7-
#[command(version = "1.0")]
8-
#[command(about = "Manually and Automated testing starting with APIs", long_about = None)]
7+
#[command(
8+
name = "testkit",
9+
author = "APIToolkit. <[email protected]>",
10+
version = "1.0",
11+
about = "Manually and Automated testing starting with APIs and Browser automation",
12+
long_about = None
13+
)]
914
pub struct Cli {
1015
#[command(subcommand)]
1116
pub command: Option<Commands>,
@@ -31,6 +36,15 @@ pub enum Commands {
3136
#[arg(short, long)]
3237
file: Option<PathBuf>,
3338
},
39+
/// Create a new boilerplate test file.
40+
New {
41+
/// Path where the boilerplate file should be created (default: boilerplate_test.yaml).
42+
#[arg(short, long)]
43+
file: Option<PathBuf>,
44+
/// Type of test file to create: "api", "browser", or "both" (default: both).
45+
#[arg(short, long, default_value = "both")]
46+
test_type: String,
47+
},
3448
/// Run the application mode (not implemented yet).
3549
App {},
3650
}

0 commit comments

Comments
 (0)