Get proof of concept working
This commit is contained in:
parent
9955517f4a
commit
46a9bd477a
14 changed files with 621 additions and 71 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
**/target
|
**/target
|
||||||
*.log
|
*.log
|
||||||
*.tjson
|
*.test.json
|
||||||
|
*.json
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
24
cef-test-core/src/harness/adapter.rs
Normal file
24
cef-test-core/src/harness/adapter.rs
Normal 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
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
/// API-specific implementation of interacting with CEF DevTools
|
|
||||||
pub trait TestAdaptor {
|
|
||||||
//TODO
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
241
cef-test-core/src/harness/headless_adapter.rs
Normal file
241
cef-test-core/src/harness/headless_adapter.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
31
examples/click_on_friends.json
Normal file
31
examples/click_on_friends.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
78
src/main.rs
78
src/main.rs
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue