Implement assertions and get basic assertion test working
This commit is contained in:
parent
46a9bd477a
commit
ba7e99f482
12 changed files with 300 additions and 71 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -154,6 +154,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"headless_chrome",
|
"headless_chrome",
|
||||||
"log 0.4.17",
|
"log 0.4.17",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ureq",
|
"ureq",
|
||||||
|
|
1
cef-test-core/Cargo.lock
generated
1
cef-test-core/Cargo.lock
generated
|
@ -144,6 +144,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"headless_chrome",
|
"headless_chrome",
|
||||||
"log 0.4.17",
|
"log 0.4.17",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ureq",
|
"ureq",
|
||||||
|
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
regex = "1"
|
||||||
chrono = { version = "0.4" }
|
chrono = { version = "0.4" }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,12 @@ pub trait TestAdapter {
|
||||||
/// Scroll to element in tab
|
/// Scroll to element in tab
|
||||||
fn element_scroll_to(&mut self, tab: &TabSelector, element: &ElementSelector) -> Feedback;
|
fn element_scroll_to(&mut self, tab: &TabSelector, element: &ElementSelector) -> Feedback;
|
||||||
|
|
||||||
|
/// Retrieve text in element in tab
|
||||||
|
fn element_value(&mut self, tab: &TabSelector, element: &ElementSelector) -> Feedback;
|
||||||
|
|
||||||
|
/// Retrieve text in element in tab
|
||||||
|
fn element_attribute(&mut self, tab: &TabSelector, element: &ElementSelector, attribute: &str) -> Feedback;
|
||||||
|
|
||||||
/// Pause execution in tab for a period
|
/// Pause execution in tab for a period
|
||||||
fn wait(&mut self, tab: &TabSelector, milliseconds: u64) -> Feedback;
|
fn wait(&mut self, tab: &TabSelector, milliseconds: u64) -> Feedback;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ pub enum Feedback {
|
||||||
Start,
|
Start,
|
||||||
/// Last instruction was successful
|
/// Last instruction was successful
|
||||||
Success,
|
Success,
|
||||||
|
/// Last instruction returned a value
|
||||||
|
Value(serde_json::Value),
|
||||||
/// Last instruction was an assertion and it failed
|
/// Last instruction was an assertion and it failed
|
||||||
AssertFailure,
|
AssertFailure,
|
||||||
/// Last instruction raised an error
|
/// Last instruction raised an error
|
||||||
|
@ -18,6 +20,7 @@ impl Feedback {
|
||||||
match self {
|
match self {
|
||||||
Self::Success => true,
|
Self::Success => true,
|
||||||
Self::Start => true,
|
Self::Start => true,
|
||||||
|
Self::Value(_) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,4 +32,14 @@ impl Feedback {
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Feedback is indicative of a failing test
|
||||||
|
pub fn is_fail(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::AssertFailure => true,
|
||||||
|
Self::Error => true,
|
||||||
|
Self::Unsupported => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,73 @@
|
||||||
use super::{TestRunner, TestAdapter, TestMetadata};
|
use super::{TestRunner, TestAdapter, TestMetadata};
|
||||||
use super::{Instruction, TestAssert, TestOp, Feedback, GeneralOpType, ElementOpType, BasicOpType};
|
use super::{Instruction, TestAssert, TestOp, Feedback, GeneralOpType, ElementOpType, TabOpType, GeneralAssertType, ElementAssertionType, TabAssert, Comparison};
|
||||||
|
|
||||||
/// Harness which runs one or more tests
|
/// Harness which runs one or more tests
|
||||||
pub struct TestHarness<R: TestRunner, A: TestAdapter> {
|
pub struct TestHarness<R: TestRunner, A: TestAdapter> {
|
||||||
tests: Vec<R>,
|
tests: Vec<R>,
|
||||||
adaptor: A,
|
adapter: A,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: TestRunner, A: TestAdapter> 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(adapter: A, tests: Vec<R>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
adaptor,
|
adapter,
|
||||||
tests,
|
tests,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_assertion(&mut self, _assertion: TestAssert) -> Feedback {
|
fn translate_assertion(&mut self, assertion: TestAssert) -> Feedback {
|
||||||
// TODO
|
match assertion.assertion {
|
||||||
Feedback::Success
|
GeneralAssertType::Element(elem) => {
|
||||||
|
match elem.assert {
|
||||||
|
ElementAssertionType::Value(comparison) =>
|
||||||
|
Self::maybe_assert(
|
||||||
|
self.adapter.element_value(&assertion.context, &elem.element),
|
||||||
|
comparison
|
||||||
|
),
|
||||||
|
ElementAssertionType::Attribute { attribute, comparison } =>
|
||||||
|
Self::maybe_assert(
|
||||||
|
self.adapter.element_attribute(&assertion.context, &elem.element, &attribute),
|
||||||
|
comparison
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GeneralAssertType::Tab(TabAssert::Evaluate { script, comparison }) => Self::maybe_assert(self.adapter.evaluate(&assertion.context, &script), comparison)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_assert(adaptor_feedback: Feedback, cmp: Comparison) -> Feedback {
|
||||||
|
if let Feedback::Value(v) = &adaptor_feedback {
|
||||||
|
if cmp.compare(Some(v)) {
|
||||||
|
log::info!("Assertion satisfied: {}", cmp.pseudocode_assert(Some(v)));
|
||||||
|
adaptor_feedback
|
||||||
|
} else {
|
||||||
|
log::error!("Assertion failed: {}", cmp.pseudocode_assert(Some(v)));
|
||||||
|
Feedback::AssertFailure
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if cmp.compare(None) {
|
||||||
|
log::info!("Assertion satisfied: {}", cmp.pseudocode_assert(None));
|
||||||
|
adaptor_feedback
|
||||||
|
} else {
|
||||||
|
log::error!("Assertion failed: {}", cmp.pseudocode_assert(None));
|
||||||
|
Feedback::AssertFailure
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_ui_op(&mut self, op: TestOp) -> Feedback {
|
fn translate_ui_op(&mut self, op: TestOp) -> Feedback {
|
||||||
// TODO
|
|
||||||
match op.op {
|
match op.op {
|
||||||
GeneralOpType::Element(elem) => {
|
GeneralOpType::Element(elem) => {
|
||||||
match elem.op {
|
match elem.op {
|
||||||
ElementOpType::Click => self.adaptor.element_click(&op.context, &elem.context),
|
ElementOpType::Click => self.adapter.element_click(&op.context, &elem.context),
|
||||||
ElementOpType::WaitFor => self.adaptor.element_wait(&op.context, &elem.context),
|
ElementOpType::WaitFor => self.adapter.element_wait(&op.context, &elem.context),
|
||||||
ElementOpType::Focus => self.adaptor.element_focus(&op.context, &elem.context),
|
ElementOpType::Focus => self.adapter.element_focus(&op.context, &elem.context),
|
||||||
ElementOpType::ScrollTo => self.adaptor.element_scroll_to(&op.context, &elem.context),
|
ElementOpType::ScrollTo => self.adapter.element_scroll_to(&op.context, &elem.context),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
GeneralOpType::Basic(BasicOpType::Sleep(ms)) => self.adaptor.wait(&op.context, ms),
|
GeneralOpType::Tab(TabOpType::Sleep(ms)) => self.adapter.wait(&op.context, ms),
|
||||||
GeneralOpType::Basic(BasicOpType::Evaluate(js)) => self.adaptor.evaluate(&op.context, &js),
|
GeneralOpType::Tab(TabOpType::Evaluate(js)) => self.adapter.evaluate(&op.context, &js),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +101,7 @@ impl<R: TestRunner, A: TestAdapter> TestHarness<R, A> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if failures.is_empty() {
|
if failures.is_empty() {
|
||||||
Ok(self.adaptor)
|
Ok(self.adapter)
|
||||||
} else {
|
} else {
|
||||||
Err(failures)
|
Err(failures)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
use headless_chrome::{Browser, Tab, Element};
|
use headless_chrome::{Browser, Tab, Element};
|
||||||
|
|
||||||
use crate::cef::WebContent;
|
use crate::cef::WebContent;
|
||||||
|
@ -29,7 +30,7 @@ impl HeadlessAdapter {
|
||||||
conn_map.insert(web_contents[0].title().to_owned(), browser);
|
conn_map.insert(web_contents[0].title().to_owned(), browser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//std::thread::sleep(std::time::Duration::from_millis(1_000));
|
std::thread::sleep(std::time::Duration::from_millis(1_000));
|
||||||
log::info!("HeadlessAdapter ready");
|
log::info!("HeadlessAdapter ready");
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
web_content: web_contents,
|
web_content: web_contents,
|
||||||
|
@ -43,8 +44,7 @@ impl HeadlessAdapter {
|
||||||
if let Some(browser) = self.connections.get(title) {
|
if let Some(browser) = self.connections.get(title) {
|
||||||
for tab in browser.get_tabs().lock().map_err(|e| format!("{}", e))?.iter() {
|
for tab in browser.get_tabs().lock().map_err(|e| format!("{}", e))?.iter() {
|
||||||
if let Ok(info) = tab.get_target_info() {
|
if let Ok(info) = tab.get_target_info() {
|
||||||
dbg!(&info.title, title == &info.title);
|
if info.title == title {
|
||||||
if title == &info.title {
|
|
||||||
return Ok(Some(tab.clone()));
|
return Ok(Some(tab.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,15 @@ impl HeadlessAdapter {
|
||||||
for web_content in self.web_content.iter() {
|
for web_content in self.web_content.iter() {
|
||||||
let is_match = match tab_select {
|
let is_match = match tab_select {
|
||||||
TabSelector::Title(title) => title == web_content.title(),
|
TabSelector::Title(title) => title == web_content.title(),
|
||||||
|
TabSelector::TitleRegex(pattern) => {
|
||||||
|
let pattern = Regex::new(pattern).map_err(|x| x.to_string())?;
|
||||||
|
pattern.is_match(web_content.title())
|
||||||
|
},
|
||||||
TabSelector::Url(url) => url == web_content.url(),
|
TabSelector::Url(url) => url == web_content.url(),
|
||||||
|
TabSelector::UrlRegex(pattern) => {
|
||||||
|
let pattern = Regex::new(pattern).map_err(|x| x.to_string())?;
|
||||||
|
pattern.is_match(web_content.url())
|
||||||
|
},
|
||||||
TabSelector::Id(id) => id == web_content.id(),
|
TabSelector::Id(id) => id == web_content.id(),
|
||||||
};
|
};
|
||||||
if is_match {
|
if is_match {
|
||||||
|
@ -81,6 +89,14 @@ impl HeadlessAdapter {
|
||||||
|| info.browser_context_id.map(|ctx_id| &ctx_id == id).unwrap_or(false)
|
|| info.browser_context_id.map(|ctx_id| &ctx_id == id).unwrap_or(false)
|
||||||
},
|
},
|
||||||
TabSelector::Title(title) => title == &info.title,
|
TabSelector::Title(title) => title == &info.title,
|
||||||
|
TabSelector::TitleRegex(pattern) => {
|
||||||
|
let pattern = Regex::new(pattern).map_err(|x| x.to_string())?;
|
||||||
|
pattern.is_match(&info.title)
|
||||||
|
},
|
||||||
|
TabSelector::UrlRegex(pattern) => {
|
||||||
|
let pattern = Regex::new(pattern).map_err(|x| x.to_string())?;
|
||||||
|
pattern.is_match(&info.url)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
if is_match {
|
if is_match {
|
||||||
return Ok(Some(tab.clone()));
|
return Ok(Some(tab.clone()));
|
||||||
|
@ -136,7 +152,7 @@ impl TestAdapter for HeadlessAdapter {
|
||||||
fn element_click(&mut self, tab_s: &TabSelector, element_s: &ElementSelector) -> Feedback {
|
fn element_click(&mut self, tab_s: &TabSelector, element_s: &ElementSelector) -> Feedback {
|
||||||
// TODO better feedback
|
// TODO better feedback
|
||||||
if let Some(tab) = self.select_tab(tab_s, true) {
|
if let Some(tab) = self.select_tab(tab_s, true) {
|
||||||
if let Some(element) = self.select_element(&tab, element_s) {
|
/*if let Some(element) = self.select_element(&tab, element_s) {
|
||||||
match element.click() {
|
match element.click() {
|
||||||
Ok(_) => Feedback::Success,
|
Ok(_) => Feedback::Success,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -147,6 +163,17 @@ impl TestAdapter for HeadlessAdapter {
|
||||||
} else {
|
} else {
|
||||||
log::error!("Failed to find element {}", element_s);
|
log::error!("Failed to find element {}", element_s);
|
||||||
Feedback::Error
|
Feedback::Error
|
||||||
|
}*/
|
||||||
|
// FIXME element.click() doesn't actually click
|
||||||
|
let result = match element_s {
|
||||||
|
ElementSelector::CSS(css) => tab.evaluate(&format!("document.querySelector(\"{}\").click()", css), true),
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Ok(_) => Feedback::Success,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to click on element {}: {}", element_s, e);
|
||||||
|
Feedback::Error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::error!("Failed to find tab {}", tab_s);
|
log::error!("Failed to find tab {}", tab_s);
|
||||||
|
@ -212,6 +239,51 @@ impl TestAdapter for HeadlessAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn element_value(&mut self, tab_s: &TabSelector, element_s: &ElementSelector) -> Feedback {
|
||||||
|
if let Some(tab) = self.select_tab(tab_s, true) {
|
||||||
|
if let Some(element) = self.select_element(&tab, element_s) {
|
||||||
|
match element.get_inner_text() {
|
||||||
|
Ok(t) => Feedback::Value(t.into()),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to get inner text value of 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_attribute(&mut self, tab_s: &TabSelector, element_s: &ElementSelector, _attribute: &str) -> Feedback {
|
||||||
|
if let Some(tab) = self.select_tab(tab_s, true) {
|
||||||
|
if let Some(element) = self.select_element(&tab, element_s) {
|
||||||
|
match element.get_attributes() {
|
||||||
|
Ok(Some(_attrs)) => {
|
||||||
|
// TODO
|
||||||
|
//attrs.get(attribute).map(|x| x.into()).unwrap_or(serde_json::Value::Null)
|
||||||
|
Feedback::Unsupported
|
||||||
|
},
|
||||||
|
Ok(None) => Feedback::Value(serde_json::Value::Null),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to get attributes of 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 {
|
fn wait(&mut self, tab_s: &TabSelector, milliseconds: u64) -> Feedback {
|
||||||
// TODO better feedback
|
// TODO better feedback
|
||||||
if let Some(_tab) = self.select_tab(tab_s, true) {
|
if let Some(_tab) = self.select_tab(tab_s, true) {
|
||||||
|
@ -227,7 +299,7 @@ impl TestAdapter for HeadlessAdapter {
|
||||||
fn evaluate(&mut self, tab_s: &TabSelector, script: &str) -> Feedback {
|
fn evaluate(&mut self, tab_s: &TabSelector, script: &str) -> Feedback {
|
||||||
if let Some(tab) = self.select_tab(tab_s, true) {
|
if let Some(tab) = self.select_tab(tab_s, true) {
|
||||||
match tab.evaluate(script, true) {
|
match tab.evaluate(script, true) {
|
||||||
Ok(_) => Feedback::Success,
|
Ok(result) => Feedback::Value(result.value.unwrap_or(serde_json::Value::Null)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to evaluate script on tab {}: {}", tab_s, e);
|
log::error!("Failed to evaluate script on tab {}: {}", tab_s, e);
|
||||||
Feedback::Error
|
Feedback::Error
|
||||||
|
|
|
@ -18,6 +18,8 @@ pub struct TestAssert {
|
||||||
pub enum GeneralAssertType {
|
pub enum GeneralAssertType {
|
||||||
/// Element-related assertion
|
/// Element-related assertion
|
||||||
Element(ElementAssert),
|
Element(ElementAssert),
|
||||||
|
/// Tab-related assertion
|
||||||
|
Tab(TabAssert),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Element assertion
|
/// Element assertion
|
||||||
|
@ -30,10 +32,125 @@ pub struct ElementAssert {
|
||||||
|
|
||||||
/// Assertion operations
|
/// Assertion operations
|
||||||
pub enum ElementAssertionType {
|
pub enum ElementAssertionType {
|
||||||
/// Assert element exists
|
/// Assert element text value
|
||||||
|
Value(Comparison),
|
||||||
|
/// Assert element attribute
|
||||||
|
Attribute {
|
||||||
|
/// Attribute name
|
||||||
|
attribute: String,
|
||||||
|
/// Assertion comparison mode
|
||||||
|
comparison: Comparison
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assertion operations
|
||||||
|
pub enum TabAssert {
|
||||||
|
/// Run javascript and validate the result
|
||||||
|
Evaluate {
|
||||||
|
/// Javascript to execute
|
||||||
|
script: String,
|
||||||
|
/// Assertion comparison mode
|
||||||
|
comparison: Comparison,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assertion compare operation to perform
|
||||||
|
pub enum Comparison {
|
||||||
|
/// Assert non-null
|
||||||
Exists,
|
Exists,
|
||||||
/// Assert element contains text
|
/// Assert not empty
|
||||||
TextEquals(String)
|
ExistsNotEmpty,
|
||||||
|
/// Assert equals expected text
|
||||||
|
TextEquals(String),
|
||||||
|
/// Assert contains expected string
|
||||||
|
TextContains(String),
|
||||||
|
/// Assert == expected value
|
||||||
|
Equals(serde_json::Value),
|
||||||
|
/// Assert actual != expected value
|
||||||
|
NotEquals(serde_json::Value),
|
||||||
|
/*
|
||||||
|
/// Assert actual < expected value
|
||||||
|
LessThan(serde_json::Value),
|
||||||
|
/// Assert actual <= expected value
|
||||||
|
LessThanEquals(serde_json::Value),
|
||||||
|
/// Assert actual > expected value
|
||||||
|
GreaterThan(serde_json::Value),
|
||||||
|
/// Assert actual >= expected value
|
||||||
|
GreaterThanEquals(serde_json::Value),
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Comparison {
|
||||||
|
/// Compare actual value
|
||||||
|
pub fn compare(&self, value: Option<&serde_json::Value>) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Exists => !value.is_none(),
|
||||||
|
Self::ExistsNotEmpty => {
|
||||||
|
if let Some(value) = value {
|
||||||
|
if let Some(s) = value.as_str() {
|
||||||
|
!s.is_empty()
|
||||||
|
} else {
|
||||||
|
!value.is_null()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
let value = value.unwrap_or(&serde_json::Value::Null);
|
||||||
|
match self {
|
||||||
|
Self::TextEquals(expected) => {
|
||||||
|
if let Some(actual) = value.as_str() {
|
||||||
|
actual.trim() == expected
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::TextContains(expected) => {
|
||||||
|
if let Some(actual) = value.as_str() {
|
||||||
|
actual.contains(expected)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::Equals(expected) => value == expected,
|
||||||
|
Self::NotEquals(expected) => value != expected,
|
||||||
|
|
||||||
|
Self::Exists => unreachable!(),
|
||||||
|
Self::ExistsNotEmpty => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display-friendly representation of the assertion with actual and expected values
|
||||||
|
pub fn pseudocode_assert(&self, value: Option<&serde_json::Value>) -> String {
|
||||||
|
match value {
|
||||||
|
Some(value) => {
|
||||||
|
match self {
|
||||||
|
Self::Exists => format!("{} must exist", value),
|
||||||
|
Self::ExistsNotEmpty => format!("{} must exist", value),
|
||||||
|
Self::TextEquals(expected) => format!("\"{}\" must equal \"{}\"", value, expected),
|
||||||
|
Self::TextContains(expected) => format!("\"{}\" must contain \"{}\"", value, expected),
|
||||||
|
Self::Equals(expected) => format!("{} == {}", value, expected),
|
||||||
|
Self::NotEquals(expected) => format!("{} != {}", value, expected),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let value = serde_json::Value::Null;
|
||||||
|
match self {
|
||||||
|
Self::Exists => format!("None must exist (contradiction!)"),
|
||||||
|
Self::ExistsNotEmpty => format!("None must exist (contradiction!)"),
|
||||||
|
Self::TextEquals(expected) => format!("\"{}\" must equal \"{}\"", value, expected),
|
||||||
|
Self::TextContains(expected) => format!("\"{}\" must contain \"{}\"", value, expected),
|
||||||
|
Self::Equals(expected) => format!("{} == {}", value, expected),
|
||||||
|
Self::NotEquals(expected) => format!("{} != {}", value, expected),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User interface interaction
|
/// User interface interaction
|
||||||
|
@ -45,6 +162,7 @@ pub struct TestOp {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Element selection mode
|
/// Element selection mode
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub enum ElementSelector {
|
pub enum ElementSelector {
|
||||||
/// Use CSS selector syntax
|
/// Use CSS selector syntax
|
||||||
CSS(String),
|
CSS(String),
|
||||||
|
@ -62,8 +180,12 @@ impl std::fmt::Display for ElementSelector {
|
||||||
pub enum TabSelector {
|
pub enum TabSelector {
|
||||||
/// Select by tab title
|
/// Select by tab title
|
||||||
Title(String),
|
Title(String),
|
||||||
|
/// Select by tab title regex pattern
|
||||||
|
TitleRegex(String),
|
||||||
/// Select by tab's current URL
|
/// Select by tab's current URL
|
||||||
Url(String),
|
Url(String),
|
||||||
|
/// Select by tab's current URL regex pattern
|
||||||
|
UrlRegex(String),
|
||||||
/// Select by tab identifier
|
/// Select by tab identifier
|
||||||
Id(String),
|
Id(String),
|
||||||
}
|
}
|
||||||
|
@ -72,7 +194,9 @@ impl std::fmt::Display for TabSelector {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Title(title) => write!(f, "Tab[title==`{}`]", title),
|
Self::Title(title) => write!(f, "Tab[title==`{}`]", title),
|
||||||
|
Self::TitleRegex(title) => write!(f, "Tab[title~=`{}`]", title),
|
||||||
Self::Url(url) => write!(f, "Tab[url==`{}`]", url),
|
Self::Url(url) => write!(f, "Tab[url==`{}`]", url),
|
||||||
|
Self::UrlRegex(url) => write!(f, "Tab[url~=`{}`]", url),
|
||||||
Self::Id(id) => write!(f, "Tab[id==`{}`]", id),
|
Self::Id(id) => write!(f, "Tab[id==`{}`]", id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,12 +206,12 @@ impl std::fmt::Display for TabSelector {
|
||||||
pub enum GeneralOpType {
|
pub enum GeneralOpType {
|
||||||
/// Operate on an element
|
/// Operate on an element
|
||||||
Element(ElementOp),
|
Element(ElementOp),
|
||||||
/// Basic context operation
|
/// Tab context operation
|
||||||
Basic(BasicOpType),
|
Tab(TabOpType),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Basic operation type
|
/// Basic operation type
|
||||||
pub enum BasicOpType {
|
pub enum TabOpType {
|
||||||
/// 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
|
/// Execute Javascript in the global tab context
|
||||||
|
|
|
@ -40,6 +40,7 @@ impl TestRunner for JsonRunner {
|
||||||
if matches!(fail_mode, FailureMode::FastFail) && !feedback.is_ok() {
|
if matches!(fail_mode, FailureMode::FastFail) && !feedback.is_ok() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
#[allow(clippy::never_loop)]
|
||||||
'step_loop: while self.step_i < self.test_data.test.len() {
|
'step_loop: while self.step_i < self.test_data.test.len() {
|
||||||
let step = &self.test_data.test[self.step_i];
|
let step = &self.test_data.test[self.step_i];
|
||||||
'op_loop: while self.op_i < step.operations.len() {
|
'op_loop: while self.op_i < step.operations.len() {
|
||||||
|
@ -59,7 +60,6 @@ impl TestRunner for JsonRunner {
|
||||||
self.op_i = 0;
|
self.op_i = 0;
|
||||||
self.step_i += 1;
|
self.step_i += 1;
|
||||||
}
|
}
|
||||||
//log::error!("JsonRunner.next(...) is UNIMPLEMENTED!");
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ 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};
|
use super::super::{TabSelector, ElementSelector, ElementOpType, ElementOp, TabOpType, GeneralOpType, GeneralAssertType, ElementAssert, ElementAssertionType, Instruction, TestAssert, TestOp, Comparison, /*TabAssert*/};
|
||||||
|
|
||||||
/// Test descriptor
|
/// Test descriptor
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -63,8 +63,8 @@ pub enum TabDescriptor {
|
||||||
impl From<TabDescriptor> for TabSelector {
|
impl From<TabDescriptor> for TabSelector {
|
||||||
fn from(value: TabDescriptor) -> Self {
|
fn from(value: TabDescriptor) -> Self {
|
||||||
match value {
|
match value {
|
||||||
TabDescriptor::Title{title: t} => Self::Title(t),
|
TabDescriptor::Title{title: t} => Self::TitleRegex(t),
|
||||||
TabDescriptor::Url{url: u} => Self::Url(u),
|
TabDescriptor::Url{url: u} => Self::UrlRegex(u),
|
||||||
TabDescriptor::Id{id: i} => Self::Id(i),
|
TabDescriptor::Id{id: i} => Self::Id(i),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,9 +84,13 @@ pub enum TestInstruction {
|
||||||
Eval {
|
Eval {
|
||||||
/// Javascript to execute
|
/// Javascript to execute
|
||||||
code: String,
|
code: String,
|
||||||
|
/*
|
||||||
|
/// Result assertion
|
||||||
|
assert: Option<Comparison>,
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
/// Assertion on an element
|
/// Assertion on an element
|
||||||
Assert(TestAssertionInstruction),
|
Assert(TestElementAssertion),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestInstruction {
|
impl TestInstruction {
|
||||||
|
@ -99,11 +103,11 @@ impl TestInstruction {
|
||||||
}),
|
}),
|
||||||
TestInstruction::Sleep { milliseconds } => Instruction::Operation(TestOp {
|
TestInstruction::Sleep { milliseconds } => Instruction::Operation(TestOp {
|
||||||
context: selector,
|
context: selector,
|
||||||
op: GeneralOpType::Basic(BasicOpType::Sleep(milliseconds)),
|
op: GeneralOpType::Tab(TabOpType::Sleep(milliseconds)),
|
||||||
}),
|
}),
|
||||||
TestInstruction::Eval { code } => Instruction::Operation(TestOp {
|
TestInstruction::Eval { code } => Instruction::Operation(TestOp {
|
||||||
context: selector,
|
context: selector,
|
||||||
op: GeneralOpType::Basic(BasicOpType::Evaluate(code)),
|
op: GeneralOpType::Tab(TabOpType::Evaluate(code)),
|
||||||
}),
|
}),
|
||||||
TestInstruction::Assert(assertion) => Instruction::Assertion(TestAssert {
|
TestInstruction::Assert(assertion) => Instruction::Assertion(TestAssert {
|
||||||
context: selector,
|
context: selector,
|
||||||
|
@ -131,13 +135,13 @@ impl From<TestElementInstruction> for ElementOp {
|
||||||
|
|
||||||
/// Test element instruction
|
/// Test element instruction
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct TestAssertionInstruction {
|
pub struct TestElementAssertion {
|
||||||
pub element: ElementDescriptor,
|
pub element: ElementDescriptor,
|
||||||
pub assert: ElementAssertion,
|
pub assert: ElementAssertion,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TestAssertionInstruction> for ElementAssert {
|
impl From<TestElementAssertion> for ElementAssert {
|
||||||
fn from(value: TestAssertionInstruction) -> Self {
|
fn from(value: TestElementAssertion) -> Self {
|
||||||
Self {
|
Self {
|
||||||
element: value.element.into(),
|
element: value.element.into(),
|
||||||
assert: value.assert.into(),
|
assert: value.assert.into(),
|
||||||
|
@ -148,6 +152,7 @@ impl From<TestAssertionInstruction> for ElementAssert {
|
||||||
/// Element descriptor
|
/// Element descriptor
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(tag = "by")]
|
#[serde(tag = "by")]
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub enum ElementDescriptor {
|
pub enum ElementDescriptor {
|
||||||
/// Use CSS selector syntax
|
/// Use CSS selector syntax
|
||||||
CSS{css: String},
|
CSS{css: String},
|
||||||
|
@ -190,15 +195,18 @@ impl From<ElementInteraction> for ElementOpType {
|
||||||
pub enum ElementAssertion {
|
pub enum ElementAssertion {
|
||||||
/// Assert element exists
|
/// Assert element exists
|
||||||
Exists,
|
Exists,
|
||||||
|
/// Assert element value equals text
|
||||||
|
TextEquals(String),
|
||||||
/// Assert element contains text
|
/// Assert element contains text
|
||||||
TextEquals(String)
|
TextContains(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ElementAssertion> for ElementAssertionType {
|
impl From<ElementAssertion> for ElementAssertionType {
|
||||||
fn from(value: ElementAssertion) -> Self {
|
fn from(value: ElementAssertion) -> Self {
|
||||||
match value {
|
match value {
|
||||||
ElementAssertion::Exists => Self::Exists,
|
ElementAssertion::Exists => Self::Value(Comparison::Exists),
|
||||||
ElementAssertion::TextEquals(t) => Self::TextEquals(t),
|
ElementAssertion::TextEquals(t) => Self::Value(Comparison::TextEquals(t)),
|
||||||
|
ElementAssertion::TextContains(t) => Self::Value(Comparison::TextContains(t)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,6 @@ pub use adapter::TestAdapter;
|
||||||
pub use feedback::Feedback;
|
pub use feedback::Feedback;
|
||||||
pub use harness::TestHarness;
|
pub use harness::TestHarness;
|
||||||
pub use headless_adapter::HeadlessAdapter;
|
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, TabOpType, ElementOp, ElementOpType, TabAssert, Comparison};
|
||||||
pub use json_runner::JsonRunner;
|
pub use json_runner::JsonRunner;
|
||||||
pub use runner::{TestRunner, TestMetadata};
|
pub use runner::{TestRunner, TestMetadata};
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
Reference in a new issue