Get proof of concept working

This commit is contained in:
NGnius (Graham) 2023-01-17 20:33:38 -05:00
parent 9955517f4a
commit 46a9bd477a
14 changed files with 621 additions and 71 deletions

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
**/target **/target
*.log *.log
*.tjson *.test.json
*.json

View file

@ -37,9 +37,11 @@ impl WebContent {
/// Retrieve WebContent information from CEF instance /// Retrieve WebContent information from CEF instance
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
pub fn load_all(domain_name: &str, port: u16) -> Result<Vec<Self>, ureq::Error> { pub fn load_all(domain_name: &str, port: u16) -> Result<Vec<Self>, ureq::Error> {
Ok(ureq::get(&format!("http://{}:{}/json", domain_name, port)) let web_contents = ureq::get(&format!("http://{}:{}/json", domain_name, port))
.call()? .call()?
.into_json()?) .into_json()?;
log::debug!("Got WebContent json: {:?}", web_contents);
Ok(web_contents)
} }
} }

View file

@ -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
}

View file

@ -1,4 +0,0 @@
/// API-specific implementation of interacting with CEF DevTools
pub trait TestAdaptor {
//TODO
}

View file

@ -1,13 +1,13 @@
use super::{TestRunner, TestAdaptor, TestMetadata}; use super::{TestRunner, TestAdapter, TestMetadata};
use super::{Instruction, TestAssert, TestOp, Feedback}; use super::{Instruction, TestAssert, TestOp, Feedback, GeneralOpType, ElementOpType, BasicOpType};
/// Harness which runs one or more tests /// Harness which runs one or more tests
pub struct TestHarness<R: TestRunner, A: TestAdaptor> { pub struct TestHarness<R: TestRunner, A: TestAdapter> {
tests: Vec<R>, tests: Vec<R>,
adaptor: A, adaptor: A,
} }
impl<R: TestRunner, A: TestAdaptor> TestHarness<R, A> { impl<R: TestRunner, A: TestAdapter> TestHarness<R, A> {
/// Construct a new test harness /// Construct a new test harness
pub fn new(adaptor: A, tests: Vec<R>) -> Self { pub fn new(adaptor: A, tests: Vec<R>) -> Self {
Self { Self {
@ -16,17 +16,28 @@ impl<R: TestRunner, A: TestAdaptor> TestHarness<R, A> {
} }
} }
fn translate_assertion(&self, _assertion: TestAssert) -> Feedback { fn translate_assertion(&mut self, _assertion: TestAssert) -> Feedback {
// TODO // TODO
Feedback::Success Feedback::Success
} }
fn translate_ui_op(&self, _op: TestOp) -> Feedback { fn translate_ui_op(&mut self, op: TestOp) -> Feedback {
// TODO // 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 { match instruction {
Instruction::Assertion(a) => self.translate_assertion(a), Instruction::Assertion(a) => self.translate_assertion(a),
Instruction::Operation(i) => self.translate_ui_op(i), Instruction::Operation(i) => self.translate_ui_op(i),
@ -35,7 +46,6 @@ impl<R: TestRunner, A: TestAdaptor> TestHarness<R, A> {
/// Perform the tests /// Perform the tests
pub fn execute(mut self) -> Result<A, Vec<TestMetadata>> { pub fn execute(mut self) -> Result<A, Vec<TestMetadata>> {
// TODO
let tests: Vec<R> = self.tests.drain(..).collect(); let tests: Vec<R> = self.tests.drain(..).collect();
let mut failures = Vec::with_capacity(tests.len()); let mut failures = Vec::with_capacity(tests.len());
for mut test in tests { for mut test in tests {

View file

@ -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<WebContent>,
connections: HashMap<String, Browser>,
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<Self, ureq::Error> {
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<Option<Arc<Tab>>, 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<Option<Arc<Tab>>, 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<Option<Arc<Tab>>, 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<Arc<Tab>> {
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<Element<'a>> {
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
}
}
}

View file

@ -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<WebContent>,
connections: HashMap<String, Browser>,
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<Self, ureq::Error> {
Ok(Self {
web_content: WebContent::load_all(domain_name, port)?,
connections: HashMap::new(),
domain_name: domain_name.to_owned(),
port_num: port,
})
}
}

View file

@ -50,6 +50,14 @@ pub enum ElementSelector {
CSS(String), 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 /// Tab selection mode
pub enum TabSelector { pub enum TabSelector {
/// Select by tab title /// Select by tab title
@ -60,6 +68,16 @@ pub enum TabSelector {
Id(String), 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 /// Test operation information
pub enum GeneralOpType { pub enum GeneralOpType {
/// Operate on an element /// Operate on an element
@ -72,12 +90,14 @@ pub enum GeneralOpType {
pub enum BasicOpType { pub enum BasicOpType {
/// Pause executing thread for time, in milliseconds /// Pause executing thread for time, in milliseconds
Sleep(u64), Sleep(u64),
/// Execute Javascript in the global tab context
Evaluate(String),
} }
/// Element manipulation operation /// Element manipulation operation
pub struct ElementOp { pub struct ElementOp {
/// Element to target /// Element to target
pub element: ElementSelector, pub context: ElementSelector,
/// Operation to perform /// Operation to perform
pub op: ElementOpType, pub op: ElementOpType,
} }
@ -88,5 +108,9 @@ pub enum ElementOpType {
Click, Click,
/// Wait for element to be created /// Wait for element to be created
WaitFor, WaitFor,
/// Focus the element
Focus,
/// Scroll the element into view
ScrollTo,
} }

View file

@ -1,9 +1,11 @@
use super::super::{Instruction, Feedback, TestRunner, TestMetadata}; use super::super::{Instruction, Feedback, TestRunner, TestMetadata};
use super::Test; use super::{Test, FailureMode};
/// Test runner for specific JSON data structures. /// Test runner for specific JSON data structures.
pub struct JsonRunner { pub struct JsonRunner {
test_data: Test, test_data: Test,
step_i: usize,
op_i: usize,
success: bool, success: bool,
} }
@ -14,6 +16,8 @@ impl JsonRunner {
let test = serde_json::from_reader(file)?; let test = serde_json::from_reader(file)?;
Ok(Self { Ok(Self {
test_data: test, test_data: test,
step_i: 0,
op_i: 0,
success: true, success: true,
}) })
} }
@ -22,6 +26,8 @@ impl JsonRunner {
pub fn new(test: Test) -> Self { pub fn new(test: Test) -> Self {
Self { Self {
test_data: test, test_data: test,
step_i: 0,
op_i: 0,
success: true, success: true,
} }
} }
@ -29,9 +35,31 @@ impl JsonRunner {
impl TestRunner for JsonRunner { impl TestRunner for JsonRunner {
fn next(&mut self, feedback: Feedback) -> Option<Instruction> { fn next(&mut self, feedback: Feedback) -> Option<Instruction> {
// TODO
self.success = feedback.is_ok(); 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 None
} }

View file

@ -1,12 +1,14 @@
use std::convert::From;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::super::TestMetadata; use super::super::TestMetadata;
use super::super::{TabSelector, ElementSelector, ElementOpType, ElementOp, BasicOpType, GeneralOpType, GeneralAssertType, ElementAssert, ElementAssertionType, Instruction, TestAssert, TestOp};
/// Test descriptor /// Test descriptor
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Test { pub struct Test {
pub(super) info: TestInfo, pub(super) info: TestInfo,
pub(super) test: Vec<TestInstruction>, pub(super) test: Vec<TestStep>,
} }
/// Test metadata /// Test metadata
@ -16,6 +18,15 @@ pub struct TestInfo {
pub blame: String, pub blame: String,
pub id: String, pub id: String,
pub output: 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<TestInfo> for TestMetadata { impl std::convert::From<TestInfo> for TestMetadata {
@ -30,6 +41,164 @@ impl std::convert::From<TestInfo> for TestMetadata {
} }
} }
/// Test metadata /// Test step
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TestInstruction {} pub struct TestStep {
pub tab: TabDescriptor,
pub operations: Vec<TestInstruction>,
}
/// 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<TabDescriptor> 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<TestElementInstruction> 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<TestAssertionInstruction> 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<ElementDescriptor> 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<ElementInteraction> 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<ElementAssertion> for ElementAssertionType {
fn from(value: ElementAssertion) -> Self {
match value {
ElementAssertion::Exists => Self::Exists,
ElementAssertion::TextEquals(t) => Self::TextEquals(t),
}
}
}

View file

@ -1,18 +1,18 @@
//! Test execution functionality //! Test execution functionality
mod adaptor; mod adapter;
mod feedback; mod feedback;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod harness; mod harness;
mod headless_adaptor; mod headless_adapter;
mod instructions; mod instructions;
mod json_runner; mod json_runner;
mod runner; mod runner;
pub use adaptor::TestAdaptor; pub use adapter::TestAdapter;
pub use feedback::Feedback; pub use feedback::Feedback;
pub use harness::TestHarness; 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 instructions::{Instruction, TestAssert, GeneralAssertType, ElementAssert, ElementAssertionType, TestOp, ElementSelector, TabSelector, GeneralOpType, BasicOpType, ElementOp, ElementOpType};
pub use json_runner::JsonRunner; pub use json_runner::JsonRunner;
pub use runner::{TestRunner, TestMetadata}; pub use runner::{TestRunner, TestMetadata};

View file

@ -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"
}
]
}
]
}

View file

@ -7,14 +7,14 @@ use clap::Parser;
pub struct Cli { pub struct Cli {
/// CEF DevTools port /// CEF DevTools port
#[arg(short, long)] #[arg(short, long)]
port: Option<u16>, pub port: Option<u16>,
/// CEF DevTools IP address or domain /// CEF DevTools IP address or domain
#[arg(short, long)] #[arg(short, long)]
address: Option<String>, pub address: Option<String>,
/// Test file(s) /// Test file(s)
test: Vec<PathBuf>, pub test: Vec<PathBuf>,
} }
impl Cli { impl Cli {

View file

@ -1,26 +1,76 @@
mod cli; mod cli;
use simplelog::{LevelFilter, WriteLogger}; use simplelog::{LevelFilter, WriteLogger, TermLogger, CombinedLogger};
const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME"); const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME");
const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION"); const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION");
fn main() { fn main() -> Result<(), String> {
let args = cli::Cli::parse(); let args = cli::Cli::parse();
println!("Got args {:?}", &args); println!("Got args {:?}", &args);
let log_filepath = format!("./{}-{}-v{}.log", cef_test_core::util::timestamp_now(), PACKAGE_NAME, PACKAGE_VERSION); let log_filepath = format!("./{}-{}-v{}.log", cef_test_core::util::timestamp_now(), PACKAGE_NAME, PACKAGE_VERSION);
WriteLogger::init( CombinedLogger::init(vec![
#[cfg(debug_assertions)] WriteLogger::new(
{ #[cfg(debug_assertions)]
LevelFilter::Debug {
}, LevelFilter::Debug
#[cfg(not(debug_assertions))] },
{ #[cfg(not(debug_assertions))]
LevelFilter::Info {
}, LevelFilter::Info
Default::default(), },
std::fs::File::create(&log_filepath).unwrap(), Default::default(),
).expect("Couldn't init file log"); 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(())
}
} }