diff --git a/.env b/.env index 92c5810..13dad1c 100644 --- a/.env +++ b/.env @@ -1 +1,6 @@ -STATUS=201 \ No newline at end of file +STATUS=201 +APIURL=https://reqres.in +EMAIL=eve.holt@reqres.in +PASSWORD=cityslicka +TOKEN=QpwL5tke4Pnpja7X4 +USERNAME=myusername \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 25c0a53..80f4dd9 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/Examples/realworld.tk.yaml b/Examples/api/realworld.tk.yaml similarity index 100% rename from Examples/realworld.tk.yaml rename to Examples/api/realworld.tk.yaml diff --git a/Examples/rick-and-morty.tk.yaml b/Examples/api/rick-and-morty.tk.yaml similarity index 100% rename from Examples/rick-and-morty.tk.yaml rename to Examples/api/rick-and-morty.tk.yaml diff --git a/Examples/browser/browser-test.tk.yaml b/Examples/browser/browser-test.tk.yaml new file mode 100644 index 0000000..af8fa36 --- /dev/null +++ b/Examples/browser/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/Examples/browser/wikipedia.tk.yaml b/Examples/browser/wikipedia.tk.yaml new file mode 100644 index 0000000..615084a --- /dev/null +++ b/Examples/browser/wikipedia.tk.yaml @@ -0,0 +1,21 @@ +- metadata: + name: Visit Wikipedia + description: test wikipedia + headless: false + browser: safari + groups: + - group: Visit Wikipedia + steps: + - visit: 'https://www.wikipedia.org/' + - wait: 2000 + - find: '#searchInput' + click: true + - find: '#searchInput' + type_text: 'Selenium (software)' + - wait: 1000 + - find: '.pure-button-primary-progressive' + click: true + - wait: 2000 + - find: '#ca-viewsource' + click: true + - wait: 2000 \ No newline at end of file diff --git a/src/base_browser.rs b/src/base_browser.rs new file mode 100644 index 0000000..47a783c --- /dev/null +++ b/src/base_browser.rs @@ -0,0 +1,247 @@ +use std::io::{self, Write}; +use std::time::Duration; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use thirtyfour::prelude::*; +use thirtyfour::{ + ChromeCapabilities, DesiredCapabilities, EdgeCapabilities, FirefoxCapabilities, + SafariCapabilities, +}; + +enum BrowserCapabilities { + Firefox(FirefoxCapabilities), + Chrome(ChromeCapabilities), + Safari(SafariCapabilities), + Edge(EdgeCapabilities), +} + +#[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: Option = None; + + // Find the metadata to configure the browser + for (i, item) in test_cases.iter().enumerate() { + if let Some(metadata) = &item.metadata { + log::info!(target: "testkit", "running on:{:?}", metadata.browser.as_ref().unwrap()); + + driver = get_web_driver(metadata).await; + break; + } + } + + let mut all_results = Vec::new(); + + if driver.is_none() { + log::info!(target:"testkit", "no driver configuration found in metadata"); + } else { + for test_case in test_cases { + let result = base_browser(test_case, driver.clone().unwrap()).await; + match result { + Ok(mut res) => { + if should_log { + log::info!(target:"testkit", "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() { + 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) +} + +async fn get_web_driver(metadata: &Metadata) -> Option { + let port = "http://localhost:4444"; + match metadata.browser { + Some(ref browser_str) => { + let caps: Option = match browser_str.as_str() { + "firefox" => { + log::info!(target: "testkit", "initializing Firefox"); + let mut caps = DesiredCapabilities::firefox(); + if metadata.headless.unwrap_or(false) { + caps.set_headless().unwrap(); + } + Some(BrowserCapabilities::Firefox(caps)) + } + "chrome" => { + log::info!(target: "testkit", "initializing Chrome"); + let mut caps = DesiredCapabilities::chrome(); + if metadata.headless.unwrap_or(false) { + caps.set_headless().unwrap(); + } + Some(BrowserCapabilities::Chrome(caps)) + } + "safari" => { + log::info!(target: "testkit", "initializing Safari"); + let mut user_prompt = None; + if metadata.headless.unwrap_or(false) { + // Reference: https://github.com/SeleniumHQ/selenium/issues/5985 + log::error!(target: "testkit", "safari driver has no headless mode support"); + user_prompt = + prompt_user("Do you want to continue without headless mode? (y/n) ") + } + match user_prompt { + Some('n') | Some('N') => None, + _ => Some(BrowserCapabilities::Safari(DesiredCapabilities::safari())), + } + } + "edge" => { + log::info!(target: "testkit", "initializing Edge"); + let mut caps = DesiredCapabilities::edge(); + if metadata.headless.unwrap_or(false) { + caps.set_headless().unwrap(); + } + Some(BrowserCapabilities::Edge(caps)) + } + _ => { + log::info!(target: "testkit", + "unrecognized browser '{}', defaulting to Firefox", + browser_str + ); + let mut caps = DesiredCapabilities::firefox(); + if metadata.headless.unwrap_or(false) { + caps.set_headless().unwrap(); + } + Some(BrowserCapabilities::Firefox(caps)) + } + }; + + match caps { + Some(BrowserCapabilities::Chrome(chrome_caps)) => { + Some(WebDriver::new(port, chrome_caps).await.unwrap()) + } + Some(BrowserCapabilities::Firefox(firefox_caps)) => { + Some(WebDriver::new(port, firefox_caps).await.unwrap()) + } + Some(BrowserCapabilities::Safari(safari_caps)) => { + Some(WebDriver::new(port, safari_caps).await.unwrap()) + } + Some(BrowserCapabilities::Edge(edge_caps)) => { + Some(WebDriver::new(port, edge_caps).await.unwrap()) + } + _ => None, + } + } + None => { + log::info!("No browser specified, defaulting to Firefox"); + let mut firefox_caps = DesiredCapabilities::firefox(); + if metadata.headless.unwrap_or(false) { + firefox_caps.set_headless().unwrap(); + } + Some(WebDriver::new(port, firefox_caps).await.unwrap()) + } + } +} + +fn prompt_user(prompt: &str) -> Option { + log::info!("{}", prompt); + io::stdout().flush().expect("Failed to flush stdout"); + + let mut input = String::new(); + if io::stdin().read_line(&mut input).is_ok() { + input.chars().next() + } else { + None + } +} 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 78878f7..1657540 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/main.rs b/src/main.rs index d309f49..02bd21c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ pub mod base_cli; +pub mod base_browser; pub mod base_request; + use anyhow::Ok; +use base_browser::TestItem; use base_cli::Commands; use base_request::TestContext; use clap::Parser; @@ -31,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())?; @@ -63,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()) {