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
|
||||
*.log
|
||||
*.tjson
|
||||
*.test.json
|
||||
*.json
|
||||
|
|
|
@ -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<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()?
|
||||
.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::{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<R: TestRunner, A: TestAdaptor> {
|
||||
pub struct TestHarness<R: TestRunner, A: TestAdapter> {
|
||||
tests: Vec<R>,
|
||||
adaptor: A,
|
||||
}
|
||||
|
||||
impl<R: TestRunner, A: TestAdaptor> TestHarness<R, A> {
|
||||
impl<R: TestRunner, A: TestAdapter> TestHarness<R, A> {
|
||||
/// Construct a new test harness
|
||||
pub fn new(adaptor: A, tests: Vec<R>) -> 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
|
||||
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<R: TestRunner, A: TestAdaptor> TestHarness<R, A> {
|
|||
|
||||
/// Perform the tests
|
||||
pub fn execute(mut self) -> Result<A, Vec<TestMetadata>> {
|
||||
// TODO
|
||||
let tests: Vec<R> = self.tests.drain(..).collect();
|
||||
let mut failures = Vec::with_capacity(tests.len());
|
||||
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),
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Instruction> {
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<TestInstruction>,
|
||||
pub(super) test: Vec<TestStep>,
|
||||
}
|
||||
|
||||
/// 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<TestInfo> for TestMetadata {
|
||||
|
@ -30,6 +41,164 @@ impl std::convert::From<TestInfo> 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<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
|
||||
|
||||
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};
|
||||
|
|
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 {
|
||||
/// CEF DevTools port
|
||||
#[arg(short, long)]
|
||||
port: Option<u16>,
|
||||
pub port: Option<u16>,
|
||||
|
||||
/// CEF DevTools IP address or domain
|
||||
#[arg(short, long)]
|
||||
address: Option<String>,
|
||||
pub address: Option<String>,
|
||||
|
||||
/// Test file(s)
|
||||
test: Vec<PathBuf>,
|
||||
pub test: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
|
|
58
src/main.rs
58
src/main.rs
|
@ -1,17 +1,18 @@
|
|||
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(
|
||||
CombinedLogger::init(vec![
|
||||
WriteLogger::new(
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
LevelFilter::Debug
|
||||
|
@ -22,5 +23,54 @@ fn main() {
|
|||
},
|
||||
Default::default(),
|
||||
std::fs::File::create(&log_filepath).unwrap(),
|
||||
).expect("Couldn't init file log");
|
||||
),
|
||||
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