diff --git a/.DS_Store b/.DS_Store index 1f09972..4bf558b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 7a83b9c..712b023 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ node_modules/ .vscode .env +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f6d9a61..bcf04e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,11 @@ serde_with = "3.0.0" colored_json = "5" chrono = "0.4.26" walkdir = "2.3.3" +thirtyfour = "0.32.0" # core-foundation = {git="https://github.com/servo/core-foundation-rs", rev="9effb788767458ad639ce36229cc07fd3b1dc7ba"} [dev-dependencies] httpmock = "0.7" testing_logger = "0.1.1" + +[workspace] \ No newline at end of file diff --git a/browser-test.tk.yaml b/browser-test.tk.yaml new file mode 100644 index 0000000..af8fa36 --- /dev/null +++ b/browser-test.tk.yaml @@ -0,0 +1,27 @@ +- metadata: + name: Login To Talstack + description: test login + headless: false + browser: firefox + groups: + - group: Login to Talstack + steps: + - visit: 'app.talstack.com' + - find: .social-button_SocialButton__C6hcE + click: true + - wait: 2000 + - find_xpath: '//*[@id="identifierId"]' + type_text: random@tjs.com + - wait: 2000 + - find_xpath: '//*[@id="identifierNext"]' + click: true + - wait: 2000 + - find_xpath: '//*[@id="password"]/div[1]/div/div[1]/input' + type_text: "loleremm" + - find_xpath: '//*[@id="passwordNext"]' + click: true + - wait: 10000 + - group: Register a user + steps: + - visit: 'app.talstack.com/' + diff --git a/src/base_browser.rs b/src/base_browser.rs new file mode 100644 index 0000000..8fe802a --- /dev/null +++ b/src/base_browser.rs @@ -0,0 +1,181 @@ +use std::time::Duration; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use thirtyfour::prelude::*; +use thirtyfour::DesiredCapabilities; + +#[derive(Deserialize, Serialize, Debug)] +pub struct TestStep { + visit: Option, + find: Option, + find_xpath: Option, + #[serde(default)] + type_text: Option, + #[serde(default)] + click: Option, + #[serde(default)] + wait: Option, + assert: Option>, +} +#[derive(Debug, Serialize, Deserialize)] +pub struct Assertion { + array: Option, + array_xpath: Option, + empty: Option, + empty_xpath: Option, + string: Option, + string_xpath: Option, + equal: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TestItem { + metadata: Option, + groups: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Metadata { + name: Option, + description: Option, + headless: Option, + browser: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Group { + group: String, + steps: Vec, +} + +#[derive(Debug, Default, Serialize)] +pub struct RequestResult { + pub step_name: Option, + pub step_index: u32, +} +pub async fn run_browser( + test_cases: &Vec, + should_log: bool, +) -> Result, Box> { + let mut driver = None; + + // Find the metadata to configure the browser + for (i, item) in test_cases.iter().enumerate() { + if let Some(metadata) = &item.metadata { + log::debug!("running on : {:?}", metadata.browser); + + driver = match &metadata.browser { + Some(browser_str) => { + let caps = match browser_str.as_str() { + "firefox" => { + println!("Initializing Firefox"); + let mut caps = DesiredCapabilities::firefox(); + if metadata.headless.unwrap_or(false) { + caps.set_headless()?; + } + caps + } + _ => { + println!( + "Unrecognized browser '{}', defaulting to Firefox", + browser_str + ); + let mut caps = DesiredCapabilities::firefox(); + if metadata.headless.unwrap_or(false) { + caps.set_headless()?; + } + caps + } + }; + + Some(WebDriver::new("http://localhost:4444", caps).await?) + } + None => { + println!("No browser specified, defaulting to Firefox"); + let mut caps = DesiredCapabilities::firefox(); + if metadata.headless.unwrap_or(false) { + caps.set_headless()?; + } + Some(WebDriver::new("http://localhost:4444", caps).await?) + } + }; + + break; + } + } + + if driver.is_none() { + log::debug!("No driver configuration found in metadata"); + } + + let driver = driver.unwrap(); + + let mut all_results = Vec::new(); + + for test_case in test_cases { + let result = base_browser(test_case, driver.clone()).await; + match result { + Ok(mut res) => { + if should_log { + log::debug!("Test passed: {:?}", res); + } + all_results.append(&mut res); + } + Err(err) => { + if should_log { + log::error!(target: "testkit", "{}", err); + } + return Err(err); + } + } + } + + Ok(all_results) +} + +pub async fn base_browser( + test_item: &TestItem, + client: WebDriver, +) -> Result, Box> { + let mut results: Vec = Vec::new(); + + for (i, group) in test_item.groups.iter().enumerate() { + println!("Running group: {:?}", group.group); + + for (j, step) in group.steps.iter().enumerate() { + if let Some(url) = &step.visit { + client.get(url).await?; + } + if let Some(selector) = &step.find { + let element = client.find(By::Css(selector)).await?; + if step.click.unwrap_or(false) { + element.click().await?; + } + if let Some(text) = &step.type_text { + element.send_keys(text).await?; + } + } + if let Some(xpath) = &step.find_xpath { + let element = client.find(By::XPath(xpath)).await?; + if step.click.unwrap_or(false) { + element.click().await?; + } + if let Some(text) = &step.type_text { + element.send_keys(text).await?; + } + } + if let Some(wait_time) = step.wait { + tokio::time::sleep(Duration::from_millis(wait_time)).await; + } + + results.push(RequestResult { + step_name: Some(format!("{} - step {}", group.group, j)), + step_index: i as u32, + }); + } + } + + client.quit().await?; + Ok(results) +} diff --git a/src/base_cli.rs b/src/base_cli.rs index 7ec53e9..e2060e1 100644 --- a/src/base_cli.rs +++ b/src/base_cli.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; #[command(name = "testkit")] #[command(author = "APIToolkit. ")] #[command(version = "1.0")] -#[command(about = "Manually and Automated testing starting with APIs", long_about = None)] +#[command(about = "Manually and Automated testing starting with APIs and Browser", long_about = None)] pub struct Cli { #[command(subcommand)] pub command: Option, @@ -18,6 +18,13 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { Test { + /// Run browser tests + #[arg(short = 'i', long)] + api: bool, + + #[arg(short = 'b', long)] + browser: bool, + /// Sets the YAML test configuration file #[arg(short, long)] file: Option, diff --git a/src/base_request.rs b/src/base_request.rs index 14fb5f1..472658e 100644 --- a/src/base_request.rs +++ b/src/base_request.rs @@ -6,8 +6,7 @@ use reqwest::header::{HeaderMap, HeaderValue}; use rhai::Engine; use serde::{Deserialize, Serialize}; use serde_json::Value; -use serde_with::{serde_as, DisplayFromStr}; -use serde_yaml::with; +use serde_with::serde_as; use std::{collections::HashMap, env, env::VarError}; use thiserror::Error; diff --git a/src/lib.rs b/src/lib.rs index 2f83334..9f1c4a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ use base_request::{RequestResult, TestContext}; use libc::c_char; -use std::ffi::{CStr, CString}; +use std::ffi::CStr; pub mod base_cli; pub mod base_request; diff --git a/src/main.rs b/src/main.rs index f60a327..1470443 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ -#![feature(extend_one)] +pub mod base_browser; pub mod base_cli; pub mod base_request; + use anyhow::Ok; +use base_browser::TestItem; use base_cli::Commands; use base_request::{RequestResult, TestContext}; use clap::Parser; @@ -32,11 +34,18 @@ async fn main() { match cli_instance.command { None | Some(Commands::App {}) => {} - Some(Commands::Test { file }) => cli(file).await.unwrap(), + Some(Commands::Test { file, api, browser }) => { + if api { + cli_api(file.clone()).await.unwrap(); + } + if browser { + cli_browser(file).await.unwrap(); + } + } } } -async fn cli(file_op: Option) -> Result<(), anyhow::Error> { +async fn cli_api(file_op: Option) -> Result<(), anyhow::Error> { match file_op { Some(file) => { let content = fs::read_to_string(file.clone())?; @@ -64,6 +73,27 @@ async fn cli(file_op: Option) -> Result<(), anyhow::Error> { } } +async fn cli_browser(file_op: Option) -> Result<(), anyhow::Error> { + match file_op { + Some(file) => { + let content = fs::read_to_string(file.clone()).expect("Unable to read file"); + let test_cases: Vec = + serde_yaml::from_str(&content).expect("Unable to parse YAML"); + let _ = base_browser::run_browser(&test_cases, true).await; + } + None => { + let files = find_tk_yaml_files(Path::new(".")); + for file in files { + let content = fs::read_to_string(file.clone()).expect("Unable to read file"); + let test_cases: Vec = + serde_yaml::from_str(&content).expect("Unable to parse YAML"); + base_browser::run_browser(&test_cases, true).await; + } + } + } + Ok(()) +} + fn find_tk_yaml_files(dir: &Path) -> Vec { let mut result = Vec::new(); for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {