diff --git a/.gitignore b/.gitignore index 95f16ea..cc959ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ **/target *.log -*.tjson +*.test.json +*.json diff --git a/cef-test-core/src/cef/web_content.rs b/cef-test-core/src/cef/web_content.rs index dccade3..4f86e09 100644 --- a/cef-test-core/src/cef/web_content.rs +++ b/cef-test-core/src/cef/web_content.rs @@ -37,9 +37,11 @@ impl WebContent { /// Retrieve WebContent information from CEF instance #[allow(clippy::result_large_err)] pub fn load_all(domain_name: &str, port: u16) -> Result, ureq::Error> { - Ok(ureq::get(&format!("http://{}:{}/json", domain_name, port)) + let web_contents = ureq::get(&format!("http://{}:{}/json", domain_name, port)) .call()? - .into_json()?) + .into_json()?; + log::debug!("Got WebContent json: {:?}", web_contents); + Ok(web_contents) } } diff --git a/cef-test-core/src/harness/adapter.rs b/cef-test-core/src/harness/adapter.rs new file mode 100644 index 0000000..cdc6fab --- /dev/null +++ b/cef-test-core/src/harness/adapter.rs @@ -0,0 +1,24 @@ +use super::Feedback; +use super::{TabSelector, ElementSelector}; + +/// API-specific implementation of interacting with CEF DevTools +pub trait TestAdapter { + /// Click on element in tab + fn element_click(&mut self, tab: &TabSelector, element: &ElementSelector) -> Feedback; + + /// Wait for element to appear in tab + fn element_wait(&mut self, tab: &TabSelector, element: &ElementSelector) -> Feedback; + + /// Focus on element in tab + fn element_focus(&mut self, tab: &TabSelector, element: &ElementSelector) -> Feedback; + + /// Scroll to element in tab + fn element_scroll_to(&mut self, tab: &TabSelector, element: &ElementSelector) -> Feedback; + + /// Pause execution in tab for a period + fn wait(&mut self, tab: &TabSelector, milliseconds: u64) -> Feedback; + + /// Run Javascript in tab + fn evaluate(&mut self, tab: &TabSelector, script: &str) -> Feedback; + // TODO +} diff --git a/cef-test-core/src/harness/adaptor.rs b/cef-test-core/src/harness/adaptor.rs deleted file mode 100644 index 66de626..0000000 --- a/cef-test-core/src/harness/adaptor.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// API-specific implementation of interacting with CEF DevTools -pub trait TestAdaptor { - //TODO -} diff --git a/cef-test-core/src/harness/harness.rs b/cef-test-core/src/harness/harness.rs index c2460f2..3322694 100644 --- a/cef-test-core/src/harness/harness.rs +++ b/cef-test-core/src/harness/harness.rs @@ -1,13 +1,13 @@ -use super::{TestRunner, TestAdaptor, TestMetadata}; -use super::{Instruction, TestAssert, TestOp, Feedback}; +use super::{TestRunner, TestAdapter, TestMetadata}; +use super::{Instruction, TestAssert, TestOp, Feedback, GeneralOpType, ElementOpType, BasicOpType}; /// Harness which runs one or more tests -pub struct TestHarness { +pub struct TestHarness { tests: Vec, adaptor: A, } -impl TestHarness { +impl TestHarness { /// Construct a new test harness pub fn new(adaptor: A, tests: Vec) -> Self { Self { @@ -16,17 +16,28 @@ impl TestHarness { } } - fn translate_assertion(&self, _assertion: TestAssert) -> Feedback { + fn translate_assertion(&mut self, _assertion: TestAssert) -> Feedback { // TODO Feedback::Success } - fn translate_ui_op(&self, _op: TestOp) -> Feedback { + fn translate_ui_op(&mut self, op: TestOp) -> Feedback { // TODO - Feedback::Success + match op.op { + GeneralOpType::Element(elem) => { + match elem.op { + ElementOpType::Click => self.adaptor.element_click(&op.context, &elem.context), + ElementOpType::WaitFor => self.adaptor.element_wait(&op.context, &elem.context), + ElementOpType::Focus => self.adaptor.element_focus(&op.context, &elem.context), + ElementOpType::ScrollTo => self.adaptor.element_scroll_to(&op.context, &elem.context), + } + }, + GeneralOpType::Basic(BasicOpType::Sleep(ms)) => self.adaptor.wait(&op.context, ms), + GeneralOpType::Basic(BasicOpType::Evaluate(js)) => self.adaptor.evaluate(&op.context, &js), + } } - fn translate_instruction(&self, instruction: Instruction) -> Feedback { + fn translate_instruction(&mut self, instruction: Instruction) -> Feedback { match instruction { Instruction::Assertion(a) => self.translate_assertion(a), Instruction::Operation(i) => self.translate_ui_op(i), @@ -35,7 +46,6 @@ impl TestHarness { /// Perform the tests pub fn execute(mut self) -> Result> { - // TODO let tests: Vec = self.tests.drain(..).collect(); let mut failures = Vec::with_capacity(tests.len()); for mut test in tests { diff --git a/cef-test-core/src/harness/headless_adapter.rs b/cef-test-core/src/harness/headless_adapter.rs new file mode 100644 index 0000000..fab5115 --- /dev/null +++ b/cef-test-core/src/harness/headless_adapter.rs @@ -0,0 +1,241 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use headless_chrome::{Browser, Tab, Element}; + +use crate::cef::WebContent; +use super::{TestAdapter, TabSelector, ElementSelector, Feedback}; + +/// Headless Chrome Adapter for CEF +pub struct HeadlessAdapter { + web_content: Vec, + connections: HashMap, + domain_name: String, + port_num: u16 +} + +impl HeadlessAdapter { + /// Connect DevTools and prepare to connect to the browser + #[allow(clippy::result_large_err)] + pub fn connect(domain_name: &str, port: u16) -> Result { + let web_contents = WebContent::load_all(domain_name, port)?; + // connect to one tab if possible, to give DevTools time to register tabs + let mut conn_map = HashMap::new(); + if !web_contents.is_empty() { + if let Ok(browser) = Browser::connect(web_contents[0].debug_url().to_owned()).map_err(|e| format!("{}", e)) { + if let Ok(version_info) = browser.get_version() { + log::info!("CEF Adapter running (protocol {}, product {}, rev {}, js {}, user agent {})", version_info.protocol_version, version_info.product, version_info.revision, version_info.js_version, version_info.user_agent); + } + conn_map.insert(web_contents[0].title().to_owned(), browser); + } + } + //std::thread::sleep(std::time::Duration::from_millis(1_000)); + log::info!("HeadlessAdapter ready"); + Ok(Self { + web_content: web_contents, + connections: conn_map, + domain_name: domain_name.to_owned(), + port_num: port, + }) + } + + fn tab_title(&self, title: &str) -> Result>, String> { + if let Some(browser) = self.connections.get(title) { + for tab in browser.get_tabs().lock().map_err(|e| format!("{}", e))?.iter() { + if let Ok(info) = tab.get_target_info() { + dbg!(&info.title, title == &info.title); + if title == &info.title { + return Ok(Some(tab.clone())); + } + } + } + } + Ok(None) + } + + fn tab_web_content(&mut self, tab_select: &TabSelector) -> Result>, String> { + for web_content in self.web_content.iter() { + let is_match = match tab_select { + TabSelector::Title(title) => title == web_content.title(), + TabSelector::Url(url) => url == web_content.url(), + TabSelector::Id(id) => id == web_content.id(), + }; + if is_match { + let new_browser = Browser::connect(web_content.debug_url().to_owned()).map_err(|e| format!("{}", e))?; + self.connections.insert(web_content.title().to_owned(), new_browser); + return self.tab_title(web_content.title()); + } + } + Ok(None) + } + + fn tab_connection(&mut self, tab_select: &TabSelector) -> Result>, String> { + for (_title, browser) in self.connections.iter() { + for tab in browser.get_tabs().lock().map_err(|e| format!("{}", e))?.iter() { + if let Ok(info) = tab.get_target_info() { + let is_match = match tab_select { + TabSelector::Url(url) => url == &info.url, + TabSelector::Id(id) => { + id == &info.target_id + || info.opener_id.map(|opener| &opener == id).unwrap_or(false) + || info.browser_context_id.map(|ctx_id| &ctx_id == id).unwrap_or(false) + }, + TabSelector::Title(title) => title == &info.title, + }; + if is_match { + return Ok(Some(tab.clone())); + } + } + } + } + Ok(None) + } + + fn select_tab(&mut self, tab: &TabSelector, can_refresh: bool) -> Option> { + let mut tab_result = None; + match self.tab_connection(tab) { + Ok(tab) => tab_result = tab, + Err(e) => log::warn!("Failed to retrieve tab {} by connections: {}", tab, e), + } + if tab_result.is_some() { + return tab_result; + } + match self.tab_web_content(tab) { + Ok(tab) => tab_result = tab, + Err(e) => log::warn!("Failed to retrieve tab {} by web content: {}", tab, e), + } + if tab_result.is_some() || !can_refresh { + tab_result + } else { + log::info!("Tab not found, refreshing WebContent"); + match WebContent::load_all(&self.domain_name, self.port_num) { + Ok(content) => { + self.web_content = content; + self.select_tab(tab, false) + }, + Err(e) => { + log::warn!("Failed to refresh WebContent: {}", e); + None + } + } + } + } + + fn select_element<'a>(&mut self, tab: &'a Tab, element: &ElementSelector) -> Option> { + match element { + ElementSelector::CSS(css) => { + tab.wait_for_element(css) + .map_err(|e| log::error!("Failed to retrieve element {}: {}", element, e)) + .ok() + }, + } + } +} + +impl TestAdapter for HeadlessAdapter { + fn element_click(&mut self, tab_s: &TabSelector, element_s: &ElementSelector) -> Feedback { + // TODO better feedback + if let Some(tab) = self.select_tab(tab_s, true) { + if let Some(element) = self.select_element(&tab, element_s) { + match element.click() { + Ok(_) => Feedback::Success, + Err(e) => { + log::error!("Failed to click on element {}: {}", element_s, e); + Feedback::Error + } + } + } else { + log::error!("Failed to find element {}", element_s); + Feedback::Error + } + } else { + log::error!("Failed to find tab {}", tab_s); + Feedback::Error + } + } + + fn element_wait(&mut self, tab_s: &TabSelector, element_s: &ElementSelector) -> Feedback { + // TODO better feedback + if let Some(tab) = self.select_tab(tab_s, true) { + if let Some(_element) = self.select_element(&tab, element_s) { + // nothing to do -- select_element already waits + Feedback::Success + } else { + log::error!("Failed to find element {}", element_s); + Feedback::Error + } + } else { + log::error!("Failed to find tab {}", tab_s); + Feedback::Error + } + } + + fn element_focus(&mut self, tab_s: &TabSelector, element_s: &ElementSelector) -> Feedback { + // TODO better feedback + if let Some(tab) = self.select_tab(tab_s, true) { + if let Some(element) = self.select_element(&tab, element_s) { + match element.focus() { + Ok(_) => Feedback::Success, + Err(e) => { + log::error!("Failed to click on element {}: {}", element_s, e); + Feedback::Error + } + } + } else { + log::error!("Failed to find element {}", element_s); + Feedback::Error + } + } else { + log::error!("Failed to find tab {}", tab_s); + Feedback::Error + } + } + + fn element_scroll_to(&mut self, tab_s: &TabSelector, element_s: &ElementSelector) -> Feedback { + // TODO better feedback + if let Some(tab) = self.select_tab(tab_s, true) { + if let Some(element) = self.select_element(&tab, element_s) { + match element.scroll_into_view() { + Ok(_) => Feedback::Success, + Err(e) => { + log::error!("Failed to click on element {}: {}", element_s, e); + Feedback::Error + } + } + } else { + log::error!("Failed to find element {}", element_s); + Feedback::Error + } + } else { + log::error!("Failed to find tab {}", tab_s); + Feedback::Error + } + } + + fn wait(&mut self, tab_s: &TabSelector, milliseconds: u64) -> Feedback { + // TODO better feedback + if let Some(_tab) = self.select_tab(tab_s, true) { + let duration = std::time::Duration::from_millis(milliseconds); + std::thread::sleep(duration); + Feedback::Success + } else { + log::error!("Failed to find tab {}", tab_s); + Feedback::Error + } + } + + fn evaluate(&mut self, tab_s: &TabSelector, script: &str) -> Feedback { + if let Some(tab) = self.select_tab(tab_s, true) { + match tab.evaluate(script, true) { + Ok(_) => Feedback::Success, + Err(e) => { + log::error!("Failed to evaluate script on tab {}: {}", tab_s, e); + Feedback::Error + } + } + } else { + log::error!("Failed to find tab {}", tab_s); + Feedback::Error + } + } +} diff --git a/cef-test-core/src/harness/headless_adaptor.rs b/cef-test-core/src/harness/headless_adaptor.rs deleted file mode 100644 index cb25dbd..0000000 --- a/cef-test-core/src/harness/headless_adaptor.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::collections::HashMap; - -use headless_chrome::Browser; - -use crate::cef::WebContent; - -/// Headless Chrome Adaptor for CEF -pub struct HeadlessAdaptor { - web_content: Vec, - connections: HashMap, - domain_name: String, - port_num: u16 -} - -impl HeadlessAdaptor { - /// Connect DevTools and prepare to connect to the browser - #[allow(clippy::result_large_err)] - pub fn connect(domain_name: &str, port: u16) -> Result { - Ok(Self { - web_content: WebContent::load_all(domain_name, port)?, - connections: HashMap::new(), - domain_name: domain_name.to_owned(), - port_num: port, - }) - } -} diff --git a/cef-test-core/src/harness/instructions.rs b/cef-test-core/src/harness/instructions.rs index 039975a..fd3ccde 100644 --- a/cef-test-core/src/harness/instructions.rs +++ b/cef-test-core/src/harness/instructions.rs @@ -50,6 +50,14 @@ pub enum ElementSelector { CSS(String), } +impl std::fmt::Display for ElementSelector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CSS(pattern) => write!(f, "Element[css~`{}`]", pattern), + } + } +} + /// Tab selection mode pub enum TabSelector { /// Select by tab title @@ -60,6 +68,16 @@ pub enum TabSelector { Id(String), } +impl std::fmt::Display for TabSelector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Title(title) => write!(f, "Tab[title==`{}`]", title), + Self::Url(url) => write!(f, "Tab[url==`{}`]", url), + Self::Id(id) => write!(f, "Tab[id==`{}`]", id), + } + } +} + /// Test operation information pub enum GeneralOpType { /// Operate on an element @@ -72,12 +90,14 @@ pub enum GeneralOpType { pub enum BasicOpType { /// Pause executing thread for time, in milliseconds Sleep(u64), + /// Execute Javascript in the global tab context + Evaluate(String), } /// Element manipulation operation pub struct ElementOp { /// Element to target - pub element: ElementSelector, + pub context: ElementSelector, /// Operation to perform pub op: ElementOpType, } @@ -88,5 +108,9 @@ pub enum ElementOpType { Click, /// Wait for element to be created WaitFor, + /// Focus the element + Focus, + /// Scroll the element into view + ScrollTo, } diff --git a/cef-test-core/src/harness/json_runner/runner.rs b/cef-test-core/src/harness/json_runner/runner.rs index 13a18b3..ed6657c 100644 --- a/cef-test-core/src/harness/json_runner/runner.rs +++ b/cef-test-core/src/harness/json_runner/runner.rs @@ -1,9 +1,11 @@ use super::super::{Instruction, Feedback, TestRunner, TestMetadata}; -use super::Test; +use super::{Test, FailureMode}; /// Test runner for specific JSON data structures. pub struct JsonRunner { test_data: Test, + step_i: usize, + op_i: usize, success: bool, } @@ -14,6 +16,8 @@ impl JsonRunner { let test = serde_json::from_reader(file)?; Ok(Self { test_data: test, + step_i: 0, + op_i: 0, success: true, }) } @@ -22,6 +26,8 @@ impl JsonRunner { pub fn new(test: Test) -> Self { Self { test_data: test, + step_i: 0, + op_i: 0, success: true, } } @@ -29,9 +35,31 @@ impl JsonRunner { impl TestRunner for JsonRunner { fn next(&mut self, feedback: Feedback) -> Option { - // TODO self.success = feedback.is_ok(); - log::error!("JsonRunner.next(...) is UNIMPLEMENTED!"); + let fail_mode = self.test_data.info.fail_mode.clone(); + if matches!(fail_mode, FailureMode::FastFail) && !feedback.is_ok() { + return None; + } + 'step_loop: while self.step_i < self.test_data.test.len() { + let step = &self.test_data.test[self.step_i]; + 'op_loop: while self.op_i < step.operations.len() { + if matches!(fail_mode, FailureMode::SkipInstructions) && !feedback.is_ok() { + log::info!("{:?} Failing instruction, going to next step", fail_mode); + break 'op_loop; + } + let instruction = &step.operations[self.op_i]; + log::debug!("Performing step {}, operation {}", self.step_i, self.op_i); + self.op_i += 1; + return Some(instruction.clone().into_instruction(step.tab.clone())); + } + if matches!(fail_mode, FailureMode::SkipSteps) && !self.success { + log::info!("{:?} Failing step complete, ending test", fail_mode); + break 'step_loop; + } + self.op_i = 0; + self.step_i += 1; + } + //log::error!("JsonRunner.next(...) is UNIMPLEMENTED!"); None } diff --git a/cef-test-core/src/harness/json_runner/structure.rs b/cef-test-core/src/harness/json_runner/structure.rs index 14f4c1f..55128e5 100644 --- a/cef-test-core/src/harness/json_runner/structure.rs +++ b/cef-test-core/src/harness/json_runner/structure.rs @@ -1,12 +1,14 @@ +use std::convert::From; use serde::{Deserialize, Serialize}; use super::super::TestMetadata; +use super::super::{TabSelector, ElementSelector, ElementOpType, ElementOp, BasicOpType, GeneralOpType, GeneralAssertType, ElementAssert, ElementAssertionType, Instruction, TestAssert, TestOp}; /// Test descriptor #[derive(Serialize, Deserialize, Debug)] pub struct Test { pub(super) info: TestInfo, - pub(super) test: Vec, + pub(super) test: Vec, } /// Test metadata @@ -16,6 +18,15 @@ pub struct TestInfo { pub blame: String, pub id: String, pub output: String, + pub fail_mode: FailureMode, +} + +/// Failure behaviour +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum FailureMode { + SkipInstructions, + SkipSteps, + FastFail, } impl std::convert::From for TestMetadata { @@ -30,6 +41,164 @@ impl std::convert::From for TestMetadata { } } -/// Test metadata -#[derive(Serialize, Deserialize, Debug)] -pub struct TestInstruction {} +/// Test step +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TestStep { + pub tab: TabDescriptor, + pub operations: Vec, +} + +/// Tab metadata +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "by")] +pub enum TabDescriptor { + /// Select by tab title + Title{title: String}, + /// Select by tab's current URL + Url{url: String}, + /// Select by tab identifier + Id{id: String}, +} + +impl From for TabSelector { + fn from(value: TabDescriptor) -> Self { + match value { + TabDescriptor::Title{title: t} => Self::Title(t), + TabDescriptor::Url{url: u} => Self::Url(u), + TabDescriptor::Id{id: i} => Self::Id(i), + } + } +} + +/// Test instruction +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type")] +pub enum TestInstruction { + /// Operate on an element + Element(TestElementInstruction), + /// Pause executing thread for time, in milliseconds + Sleep { + /// Duration of pause + milliseconds: u64, + }, + Eval { + /// Javascript to execute + code: String, + }, + /// Assertion on an element + Assert(TestAssertionInstruction), +} + +impl TestInstruction { + pub fn into_instruction(self, tab: TabDescriptor) -> Instruction { + let selector: TabSelector = tab.into(); + match self { + TestInstruction::Element(elem) => Instruction::Operation(TestOp { + context: selector, + op: GeneralOpType::Element(elem.into()), + }), + TestInstruction::Sleep { milliseconds } => Instruction::Operation(TestOp { + context: selector, + op: GeneralOpType::Basic(BasicOpType::Sleep(milliseconds)), + }), + TestInstruction::Eval { code } => Instruction::Operation(TestOp { + context: selector, + op: GeneralOpType::Basic(BasicOpType::Evaluate(code)), + }), + TestInstruction::Assert(assertion) => Instruction::Assertion(TestAssert { + context: selector, + assertion: GeneralAssertType::Element(assertion.into()), + }), + } + } +} + +/// Test element instruction +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TestElementInstruction { + pub element: ElementDescriptor, + pub operation: ElementInteraction, +} + +impl From for ElementOp { + fn from(value: TestElementInstruction) -> Self { + Self { + context: value.element.into(), + op: value.operation.into(), + } + } +} + +/// Test element instruction +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TestAssertionInstruction { + pub element: ElementDescriptor, + pub assert: ElementAssertion, +} + +impl From for ElementAssert { + fn from(value: TestAssertionInstruction) -> Self { + Self { + element: value.element.into(), + assert: value.assert.into(), + } + } +} + +/// Element descriptor +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "by")] +pub enum ElementDescriptor { + /// Use CSS selector syntax + CSS{css: String}, +} + +impl From for ElementSelector { + fn from(value: ElementDescriptor) -> Self { + match value { + ElementDescriptor::CSS{css: s} => Self::CSS(s), + } + } +} + +/// Element operation +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ElementInteraction { + /// Click on element + Click, + /// Wait for element to be created + WaitFor, + /// Focus the element + Focus, + /// Scroll the element into view + ScrollTo, +} + +impl From for ElementOpType { + fn from(value: ElementInteraction) -> Self { + match value { + ElementInteraction::Click => Self::Click, + ElementInteraction::WaitFor => Self::WaitFor, + ElementInteraction::Focus => Self::Focus, + ElementInteraction::ScrollTo => Self::ScrollTo, + } + } +} + +/// Element operation +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ElementAssertion { + /// Assert element exists + Exists, + /// Assert element contains text + TextEquals(String) +} + +impl From for ElementAssertionType { + fn from(value: ElementAssertion) -> Self { + match value { + ElementAssertion::Exists => Self::Exists, + ElementAssertion::TextEquals(t) => Self::TextEquals(t), + } + } +} diff --git a/cef-test-core/src/harness/mod.rs b/cef-test-core/src/harness/mod.rs index 2867784..0125306 100644 --- a/cef-test-core/src/harness/mod.rs +++ b/cef-test-core/src/harness/mod.rs @@ -1,18 +1,18 @@ //! Test execution functionality -mod adaptor; +mod adapter; mod feedback; #[allow(clippy::module_inception)] mod harness; -mod headless_adaptor; +mod headless_adapter; mod instructions; mod json_runner; mod runner; -pub use adaptor::TestAdaptor; +pub use adapter::TestAdapter; pub use feedback::Feedback; pub use harness::TestHarness; -pub use headless_adaptor::HeadlessAdaptor; +pub use headless_adapter::HeadlessAdapter; pub use instructions::{Instruction, TestAssert, GeneralAssertType, ElementAssert, ElementAssertionType, TestOp, ElementSelector, TabSelector, GeneralOpType, BasicOpType, ElementOp, ElementOpType}; pub use json_runner::JsonRunner; pub use runner::{TestRunner, TestMetadata}; diff --git a/examples/click_on_friends.json b/examples/click_on_friends.json new file mode 100644 index 0000000..5ae4895 --- /dev/null +++ b/examples/click_on_friends.json @@ -0,0 +1,31 @@ +{ + "info": { + "name": "Click on Friends", + "blame": "NGnius", + "id": "DeckyTest-1", + "output": "./output.log", + "fail_mode": "SkipSteps" + }, + "test": [ + { + "tab": { + "by": "Title", + "title": "SP" + }, + "operations": [ + { + "type": "Sleep", + "milliseconds": 1000 + }, + { + "type": "Element", + "element": { + "by": "CSS", + "css": "div.gamepadtabbedpage_Tab_3eEbS.gamepadtabbedpage_HasAddon_2tufx.gamepadtabbedpage_RightAddon_KFGEk.Panel.Focusable" + }, + "operation": "Focus" + } + ] + } + ] +} diff --git a/src/cli.rs b/src/cli.rs index c30b223..e65f765 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,14 +7,14 @@ use clap::Parser; pub struct Cli { /// CEF DevTools port #[arg(short, long)] - port: Option, + pub port: Option, /// CEF DevTools IP address or domain #[arg(short, long)] - address: Option, + pub address: Option, /// Test file(s) - test: Vec, + pub test: Vec, } impl Cli { diff --git a/src/main.rs b/src/main.rs index 617a93a..5b10379 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,76 @@ mod cli; -use simplelog::{LevelFilter, WriteLogger}; +use simplelog::{LevelFilter, WriteLogger, TermLogger, CombinedLogger}; const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME"); const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION"); -fn main() { +fn main() -> Result<(), String> { let args = cli::Cli::parse(); println!("Got args {:?}", &args); let log_filepath = format!("./{}-{}-v{}.log", cef_test_core::util::timestamp_now(), PACKAGE_NAME, PACKAGE_VERSION); - WriteLogger::init( - #[cfg(debug_assertions)] - { - LevelFilter::Debug - }, - #[cfg(not(debug_assertions))] - { - LevelFilter::Info - }, - Default::default(), - std::fs::File::create(&log_filepath).unwrap(), - ).expect("Couldn't init file log"); + CombinedLogger::init(vec![ + WriteLogger::new( + #[cfg(debug_assertions)] + { + LevelFilter::Debug + }, + #[cfg(not(debug_assertions))] + { + LevelFilter::Info + }, + Default::default(), + std::fs::File::create(&log_filepath).unwrap(), + ), + TermLogger::new( + #[cfg(debug_assertions)] + { + LevelFilter::Debug + }, + #[cfg(not(debug_assertions))] + { + LevelFilter::Info + }, + Default::default(), + Default::default(), + simplelog::ColorChoice::Auto, + ) + ]).expect("Couldn't start log"); + + let (addr, port) = if let Some(addr) = args.address { + if let Some(port) = args.port { + (addr, port) + } else if addr.contains("localhost") || addr.contains("127.0.0."){ + (addr, 8080) + } else { + (addr, 8081) + } + } else { + if let Some(port) = args.port { + ("localhost".into(), port) + } else { + ("localhost".into(), 8080) + } + }; + + log::info!("Initializing test adapter"); + let adapter = cef_test_core::harness::HeadlessAdapter::connect(&addr, port).map_err(|e| e.to_string())?; + + log::info!("Initializing test runners"); + let mut runners = Vec::with_capacity(args.test.len()); + + for test_file in args.test { + runners.push(cef_test_core::harness::JsonRunner::from_file(test_file).map_err(|e| e.to_string())?); + } + log::info!("Initializing test harness"); + let harness = cef_test_core::harness::TestHarness::new(adapter, runners); + + log::info!("Starting test harness"); + if let Err(errs) = harness.execute() { + Err(format!("{} tests failed.", errs.len())) + } else { + Ok(()) + } }