siderunner 0.2.0

A library for execution of .side files produced by Selenium IDE
Documentation
#![cfg(test)]

use crate::{
    parser::{Cmd, Command},
    playground::{build_nodes, Node, Transition},
    runner::Runner,
};

#[test]
fn test_creating_run_list_basic() {
    let commands = vec![
        blank_cmd(Cmd::Open("".to_owned())),
        blank_cmd(Cmd::Echo("".to_owned())),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("".to_owned()), 0, 0, Transition::Next),
            Node::new(Cmd::Echo("".to_owned()), 1, 0, Transition::Next)
        ]
    )
}

#[test]
fn test_creating_run_list_with_commeted_commands() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::empty_custom()),
        blank_cmd(Cmd::empty_custom()),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::empty_custom()),
        blank_cmd(Cmd::Echo("echo".to_owned())),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(Cmd::Echo("echo".to_owned()), 3, 0, Transition::Next),
            Node::new(Cmd::Echo("echo".to_owned()), 5, 0, Transition::Next),
        ]
    )
}

#[test]
fn test_creating_run_list_with_commeted_command_and_while() {
    let commands = vec![
        blank_cmd(Cmd::While("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::empty_custom()),
        blank_cmd(Cmd::Echo("echo".to_owned())),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(
                Cmd::While("...".to_owned()),
                0,
                0,
                Transition::Conditional { next: 1, end: 4 }
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 1, 1, Transition::Next),
            Node::new(Cmd::End, 2, 0, Transition::Move(0)),
            Node::new(Cmd::Echo("echo".to_owned()), 4, 0, Transition::Next),
        ]
    )
}

#[test]
fn test_creating_run_list_while_loop() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::While("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(
                Cmd::While("...".to_owned()),
                1,
                0,
                Transition::Conditional { next: 2, end: 4 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 2, 1, Transition::Next),
            Node::new(Cmd::End, 3, 0, Transition::Move(1)),
        ]
    )
}

#[test]
fn test_creating_run_list_if() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::If("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::Echo("echo".to_owned())),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(
                Cmd::If("...".to_owned()),
                1,
                0,
                Transition::Conditional { next: 2, end: 3 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 2, 1, Transition::Next),
            Node::new(Cmd::End, 3, 0, Transition::Next),
            Node::new(Cmd::Echo("echo".to_owned()), 4, 0, Transition::Next),
        ]
    )
}

#[test]
fn test_creating_run_list_if_complex_empty_conditions() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::If("...".to_owned())),
        blank_cmd(Cmd::ElseIf("...".to_owned())),
        blank_cmd(Cmd::Else),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(
                Cmd::If("...".to_owned()),
                1,
                0,
                Transition::Conditional { next: 4, end: 2 },
            ),
            Node::new(
                Cmd::ElseIf("...".to_owned()),
                2,
                0,
                Transition::Conditional { next: 4, end: 3 },
            ),
            Node::new(Cmd::Else, 3, 0, Transition::Next),
            Node::new(Cmd::End, 4, 0, Transition::Next),
        ]
    )
}

#[test]
fn test_creating_run_list_if_complex() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::If("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::ElseIf("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::Else),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(
                Cmd::If("...".to_owned()),
                1,
                0,
                Transition::Conditional { next: 2, end: 3 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 2, 1, Transition::Move(8)),
            Node::new(
                Cmd::ElseIf("...".to_owned()),
                3,
                0,
                Transition::Conditional { next: 4, end: 6 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 4, 1, Transition::Next),
            Node::new(Cmd::Echo("echo".to_owned()), 5, 1, Transition::Move(8)),
            Node::new(Cmd::Else, 6, 0, Transition::Next),
            Node::new(Cmd::Echo("echo".to_owned()), 7, 1, Transition::Next),
            Node::new(Cmd::End, 8, 0, Transition::Next)
        ]
    )
}

#[test]
fn test_creating_run_list_if_complex_without_else() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::If("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::ElseIf("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);

    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(
                Cmd::If("...".to_owned()),
                1,
                0,
                Transition::Conditional { next: 2, end: 3 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 2, 1, Transition::Move(5)),
            Node::new(
                Cmd::ElseIf("...".to_owned()),
                3,
                0,
                Transition::Conditional { next: 4, end: 5 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 4, 1, Transition::Next),
            Node::new(Cmd::End, 5, 0, Transition::Next),
        ]
    )
}

#[test]
fn test_creating_run_list_multi_while() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::While("...".to_owned())),
        blank_cmd(Cmd::While("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(
                Cmd::While("...".to_owned()),
                1,
                0,
                Transition::Conditional { next: 2, end: 6 },
            ),
            Node::new(
                Cmd::While("...".to_owned()),
                2,
                1,
                Transition::Conditional { next: 3, end: 5 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 3, 2, Transition::Next),
            Node::new(Cmd::End, 4, 1, Transition::Move(2)),
            Node::new(Cmd::End, 5, 0, Transition::Move(1)),
        ]
    )
}

#[test]
fn test_creating_run_list_multi_while_with_if() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::While("...".to_owned())),
        blank_cmd(Cmd::While("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::If("...".to_owned())),
        blank_cmd(Cmd::Else),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(
                Cmd::While("...".to_owned()),
                1,
                0,
                Transition::Conditional { next: 2, end: 9 },
            ),
            Node::new(
                Cmd::While("...".to_owned()),
                2,
                1,
                Transition::Conditional { next: 3, end: 8 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 3, 2, Transition::Next),
            Node::new(
                Cmd::If("...".to_owned()),
                4,
                2,
                Transition::Conditional { next: 6, end: 5 },
            ),
            Node::new(Cmd::Else, 5, 2, Transition::Next),
            Node::new(Cmd::End, 6, 2, Transition::Next),
            Node::new(Cmd::End, 7, 1, Transition::Move(2)),
            Node::new(Cmd::End, 8, 0, Transition::Move(1)),
        ]
    )
}

#[test]
fn test_creating_run_list_while_with_if() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::While("...".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::If("...".to_owned())),
        blank_cmd(Cmd::Else),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(
                Cmd::While("...".to_owned()),
                1,
                0,
                Transition::Conditional { next: 2, end: 7 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 2, 1, Transition::Next),
            Node::new(
                Cmd::If("...".to_owned()),
                3,
                1,
                Transition::Conditional { next: 5, end: 4 },
            ),
            Node::new(Cmd::Else, 4, 1, Transition::Next),
            Node::new(Cmd::End, 5, 1, Transition::Next),
            Node::new(Cmd::End, 6, 0, Transition::Move(1)),
        ]
    )
}

#[test]
fn test_creating_run_list_repeat_if() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::Do),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::RepeatIf("...".to_owned())),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(Cmd::Do, 1, 0, Transition::Next),
            Node::new(Cmd::Echo("echo".to_owned()), 2, 1, Transition::Next),
            Node::new(
                Cmd::RepeatIf("...".to_owned()),
                3,
                0,
                Transition::Conditional { next: 1, end: 4 },
            ),
        ]
    )
}

#[test]
fn test_creating_run_list_repeat_if_with_if() {
    let commands = vec![
        blank_cmd(Cmd::Open("open".to_owned())),
        blank_cmd(Cmd::Do),
        blank_cmd(Cmd::If("".to_owned())),
        blank_cmd(Cmd::ElseIf("".to_owned())),
        blank_cmd(Cmd::Echo("echo".to_owned())),
        blank_cmd(Cmd::Else),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::RepeatIf("...".to_owned())),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("open".to_owned()), 0, 0, Transition::Next),
            Node::new(Cmd::Do, 1, 0, Transition::Next),
            Node::new(
                Cmd::If("".to_owned()),
                2,
                1,
                Transition::Conditional { next: 6, end: 3 }
            ),
            Node::new(
                Cmd::ElseIf("".to_owned()),
                3,
                1,
                Transition::Conditional { next: 4, end: 5 },
            ),
            Node::new(Cmd::Echo("echo".to_owned()), 4, 2, Transition::Move(6)),
            Node::new(Cmd::Else, 5, 1, Transition::Next),
            Node::new(Cmd::End, 6, 1, Transition::Next),
            Node::new(
                Cmd::RepeatIf("...".to_owned()),
                7,
                0,
                Transition::Conditional { next: 1, end: 8 },
            ),
        ]
    )
}

#[test]
fn test_creating_run_list_repeat_if_and_while_and_if() {
    let commands = vec![
        blank_cmd(Cmd::Open("".to_owned())),
        blank_cmd(Cmd::Do),
        blank_cmd(Cmd::While("".to_owned())),
        blank_cmd(Cmd::If("".to_owned())),
        blank_cmd(Cmd::ElseIf("".to_owned())),
        blank_cmd(Cmd::Echo("".to_owned())),
        blank_cmd(Cmd::Else),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::RepeatIf("".to_owned())),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(Cmd::Open("".to_owned()), 0, 0, Transition::Next),
            Node::new(Cmd::Do, 1, 0, Transition::Next),
            Node::new(
                Cmd::While("".to_owned()),
                2,
                1,
                Transition::Conditional { next: 3, end: 9 },
            ),
            Node::new(
                Cmd::If("".to_owned()),
                3,
                2,
                Transition::Conditional { next: 7, end: 4 },
            ),
            Node::new(
                Cmd::ElseIf("".to_owned()),
                4,
                2,
                Transition::Conditional { next: 5, end: 6 },
            ),
            Node::new(Cmd::Echo("".to_owned()), 5, 3, Transition::Move(7)),
            Node::new(Cmd::Else, 6, 2, Transition::Next),
            Node::new(Cmd::End, 7, 2, Transition::Next),
            Node::new(Cmd::End, 8, 1, Transition::Move(2)),
            Node::new(
                Cmd::RepeatIf("".to_owned()),
                9,
                0,
                Transition::Conditional { next: 1, end: 10 },
            ),
        ]
    )
}

#[test]
fn test_creating_run_list_with_while_and_repeat_if() {
    let commands = vec![
        blank_cmd(Cmd::While("..".to_owned())),
        blank_cmd(Cmd::Do),
        blank_cmd(Cmd::RepeatIf("...".to_owned())),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(
                Cmd::While("..".to_owned()),
                0,
                0,
                Transition::Conditional { next: 1, end: 4 },
            ),
            Node::new(Cmd::Do, 1, 1, Transition::Next),
            Node::new(
                Cmd::RepeatIf("...".to_owned()),
                2,
                1,
                Transition::Conditional { next: 1, end: 3 },
            ),
            Node::new(Cmd::End, 3, 0, Transition::Move(0)),
        ]
    )
}

#[test]
fn test_creating_run_list_with_2_whiles_and_2_ifs() {
    let commands = vec![
        blank_cmd(Cmd::While(String::new())),
        blank_cmd(Cmd::If(String::new())),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::While(String::new())),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::While(String::new())),
        blank_cmd(Cmd::If(String::new())),
        blank_cmd(Cmd::Else),
        blank_cmd(Cmd::End),
        blank_cmd(Cmd::End),
    ];
    let node = build_nodes(&commands);
    assert_eq!(
        node,
        vec![
            Node::new(
                Cmd::While(String::new()),
                0,
                0,
                Transition::Conditional { next: 1, end: 4 },
            ),
            Node::new(
                Cmd::If(String::new()),
                1,
                1,
                Transition::Conditional { next: 2, end: 2 }
            ),
            Node::new(Cmd::End, 2, 1, Transition::Next),
            Node::new(Cmd::End, 3, 0, Transition::Move(0)),
            Node::new(
                Cmd::While(String::new()),
                4,
                0,
                Transition::Conditional { next: 5, end: 6 }
            ),
            Node::new(Cmd::End, 5, 0, Transition::Move(4)),
            Node::new(
                Cmd::While(String::new()),
                6,
                0,
                Transition::Conditional { next: 7, end: 11 }
            ),
            Node::new(
                Cmd::If(String::new()),
                7,
                1,
                Transition::Conditional { next: 9, end: 8 }
            ),
            Node::new(Cmd::Else, 8, 1, Transition::Next),
            Node::new(Cmd::End, 9, 1, Transition::Next),
            Node::new(Cmd::End, 10, 0, Transition::Move(6)),
        ]
    )
}

#[cfg(test)]
mod flow {
    use super::*;
    use crate::parser::{Cmd, Command, File, Location, Target, Test};
    use mock::{Call, Client};
    use std::sync::{Arc, Mutex};

    #[tokio::test]
    async fn test_run() {
        let file = blank_file(vec![
            Cmd::Open("http://example.com".to_owned()),
            Cmd::Click(Target::new(Location::Css("".to_owned()))),
        ]);
        let client = Client::new();
        let mut runner = Runner::_new(client.clone());

        let res = runner.run(&file).await;
        assert!(res.is_ok());
        let calls = client.calls();
        assert_eq!(calls[Call::Goto], 1);
        assert_eq!(calls[Call::Click], 1);
    }

    #[tokio::test]
    async fn test_run_with_custom_command() {
        let file = blank_file(vec![
            Cmd::empty_custom(),
            Cmd::Open("http://example.com".to_owned()),
            Cmd::Click(Target::new(Location::Css("".to_owned()))),
        ]);
        let client = Client::new();
        let mut runner = Runner::_new(client.clone());

        let res = runner.run(&file).await;
        assert!(res.is_ok());
        let calls = client.calls();
        assert_eq!(calls[Call::Goto], 1);
        assert_eq!(calls[Call::Click], 1);
    }

    #[tokio::test]
    async fn test_for_each() {
        let file = blank_file(vec![
            Cmd::Open("http://example.com".to_owned()),
            Cmd::ForEach {
                var: "element".to_string(),
                iterator: "array".to_string(),
            },
            Cmd::Echo("${element}".to_string()),
            Cmd::End,
        ]);
        let client = Client::new();
        let mut runner = Runner::_new(client.clone());
        runner.save_value("array".to_string(), serde_json::json!(["E1", "E2", "E3"]));

        let echo_vector: Arc<Mutex<Vec<String>>> = Arc::default();
        let echo_vector1 = echo_vector.clone();
        runner.set_echo(move |e| echo_vector1.lock().unwrap().push(e.to_string()));

        let res = runner.run(&file).await;
        assert!(res.is_ok());

        assert_eq!(echo_vector.lock().unwrap().len(), 3);
        assert_eq!(echo_vector.lock().unwrap()[0], "E1");
        assert_eq!(echo_vector.lock().unwrap()[1], "E2");
        assert_eq!(echo_vector.lock().unwrap()[2], "E3");
    }

    #[tokio::test]
    async fn test_times() {
        let file = blank_file(vec![
            Cmd::Open("http://example.com".to_string()),
            Cmd::Times("4".to_string()),
            Cmd::Echo("".to_string()),
            Cmd::End,
        ]);
        let client = Client::new();
        let mut runner = Runner::_new(client.clone());

        let echo_counter: Arc<Mutex<usize>> = Arc::default();
        let echo_counter1 = echo_counter.clone();
        runner.set_echo(move |_| *echo_counter1.lock().unwrap() += 1);

        let res = runner.run(&file).await;
        assert!(res.is_ok());

        assert_eq!(*echo_counter.lock().unwrap(), 4);
    }

    #[tokio::test]
    async fn test_times_with_var() {
        let file = blank_file(vec![
            Cmd::Open("http://example.com".to_string()),
            Cmd::Times("${N}".to_string()),
            Cmd::Echo("".to_string()),
            Cmd::End,
        ]);
        let client = Client::new();
        let mut runner = Runner::_new(client.clone());
        runner.save_value("N".to_string(), 4u64.into());

        let echo_counter: Arc<Mutex<usize>> = Arc::default();
        let echo_counter1 = echo_counter.clone();
        runner.set_echo(move |_| *echo_counter1.lock().unwrap() += 1);

        let res = runner.run(&file).await;
        assert!(res.is_ok());

        assert_eq!(*echo_counter.lock().unwrap(), 4);
    }

    #[tokio::test]
    async fn test_open_relative_url() {
        let file = File::new(
            "".into(),
            "".into(),
            "http://example.com".into(),
            "".into(),
            vec![Test {
                id: String::new(),
                name: String::new(),
                commands: vec![Command::new(
                    "".to_owned(),
                    "".to_owned(),
                    Cmd::Open("/index.html".to_owned()),
                )],
            }],
        );

        let client = Client::with_functions(
            None,
            None,
            None,
            None,
            None,
            Some(|url| {
                assert_eq!(url, "http://example.com/index.html");
                Ok(())
            }),
            None,
            None,
            None,
            None,
        );
        let mut runner = Runner::_new(client.clone());

        runner.run(&file).await.unwrap();

        assert_eq!(client.calls()[Call::Goto], 1);
    }

    fn blank_file(commands: Vec<Cmd>) -> File {
        let commands = commands.into_iter().map(blank_cmd).collect();

        File {
            id: String::new(),
            name: String::new(),
            url: String::new(),
            version: String::new(),
            tests: vec![Test {
                id: String::new(),
                name: String::new(),
                commands,
            }],
        }
    }

    mod mock {
        use crate::error::RunnerErrorKind;
        use crate::webdriver::{Element as WebElement, Locator, Webdriver};
        use serde_json::Value as Json;
        use std::ops::{Index, IndexMut};
        use std::sync::{Arc, Mutex};
        use std::time::Duration;

        #[derive(Default)]
        pub struct Client {
            pub calls: Mutex<CallCount>,
            pub res_find: Option<fn() -> Result<Element, RunnerErrorKind>>,
            pub res_curr_url: Option<fn() -> Result<url::Url, RunnerErrorKind>>,
            pub res_exec: Option<fn() -> Result<Json, RunnerErrorKind>>,
            pub res_set_w_size: Option<fn() -> Result<(), RunnerErrorKind>>,
            pub res_close: Option<fn() -> Result<(), RunnerErrorKind>>,
            #[allow(clippy::type_complexity)]
            pub res_goto: Option<fn(&str) -> Result<(), RunnerErrorKind>>,
            pub res_w8_visib: Option<fn() -> Result<(), RunnerErrorKind>>,
            pub res_w8_pres: Option<fn() -> Result<(), RunnerErrorKind>>,
            pub res_w8_npres: Option<fn() -> Result<(), RunnerErrorKind>>,
            pub res_w8_edit: Option<fn() -> Result<(), RunnerErrorKind>>,
        }

        impl Client {
            pub fn new() -> Arc<Self> {
                Arc::new(Self::default())
            }

            #[allow(clippy::type_complexity)]
            #[allow(clippy::too_many_arguments)]
            #[allow(clippy::field_reassign_with_default)]
            pub fn with_functions(
                res_find: Option<fn() -> Result<Element, RunnerErrorKind>>,
                res_curr_url: Option<fn() -> Result<url::Url, RunnerErrorKind>>,
                res_exec: Option<fn() -> Result<Json, RunnerErrorKind>>,
                res_set_w_size: Option<fn() -> Result<(), RunnerErrorKind>>,
                res_close: Option<fn() -> Result<(), RunnerErrorKind>>,
                res_goto: Option<fn(&str) -> Result<(), RunnerErrorKind>>,
                res_w8_visib: Option<fn() -> Result<(), RunnerErrorKind>>,
                res_w8_pres: Option<fn() -> Result<(), RunnerErrorKind>>,
                res_w8_npres: Option<fn() -> Result<(), RunnerErrorKind>>,
                res_w8_edit: Option<fn() -> Result<(), RunnerErrorKind>>,
            ) -> Arc<Self> {
                let mut client = Self::default();
                client.res_find = res_find;
                client.res_curr_url = res_curr_url;
                client.res_exec = res_exec;
                client.res_set_w_size = res_set_w_size;
                client.res_close = res_close;
                client.res_goto = res_goto;
                client.res_w8_visib = res_w8_visib;
                client.res_w8_pres = res_w8_pres;
                client.res_w8_npres = res_w8_npres;
                client.res_w8_edit = res_w8_edit;

                Arc::new(client)
            }

            pub fn calls(&self) -> CallCount {
                self.calls.lock().unwrap().clone()
            }

            pub fn inc(&self, c: Call) {
                let mut calls = self.calls.lock().unwrap();
                calls[c] += 1;
            }
        }

        #[async_trait::async_trait]
        impl<'a> Webdriver for Arc<Client> {
            type Element = Element;

            async fn goto(&mut self, url: &str) -> Result<(), RunnerErrorKind> {
                self.res_goto.as_ref().map(|f| (f)(url));
                self.inc(Call::Goto);
                Ok(())
            }

            async fn find(&mut self, _: Locator) -> Result<Self::Element, RunnerErrorKind> {
                self.inc(Call::Find);
                Ok(Element(Arc::clone(self)))
            }

            async fn find_all(
                &mut self,
                _: Locator,
            ) -> Result<Vec<Self::Element>, RunnerErrorKind> {
                self.inc(Call::FindAll);
                Ok(vec![Element(Arc::clone(self))])
            }

            async fn wait_for_visible(
                &mut self,
                _: Locator,
                _: Duration,
            ) -> Result<(), RunnerErrorKind> {
                self.inc(Call::W8Visib);
                Ok(())
            }

            async fn wait_for_not_visible(
                &mut self,
                _: Locator,
                _: Duration,
            ) -> Result<(), RunnerErrorKind> {
                self.inc(Call::W8NotVisib);
                Ok(())
            }

            async fn wait_for_not_present(
                &mut self,
                _: Locator,
                _: Duration,
            ) -> Result<(), RunnerErrorKind> {
                self.inc(Call::W8NPres);
                Ok(())
            }

            async fn wait_for_present(
                &mut self,
                _: Locator,
                _: Duration,
            ) -> Result<(), RunnerErrorKind> {
                self.inc(Call::W8Pres);
                Ok(())
            }

            async fn wait_for_editable(
                &mut self,
                _: Locator,
                _: Duration,
            ) -> Result<(), RunnerErrorKind> {
                self.inc(Call::W8Edit);
                Ok(())
            }

            async fn wait_for_not_editable(
                &mut self,
                _: Locator,
                _: Duration,
            ) -> Result<(), RunnerErrorKind> {
                self.inc(Call::W8NotEdit);
                Ok(())
            }

            async fn current_url(&mut self) -> Result<url::Url, RunnerErrorKind> {
                self.inc(Call::CurrentUrl);
                Ok(url::Url::parse("http://example.com").unwrap())
            }

            async fn set_window_size(&mut self, _: u32, _: u32) -> Result<(), RunnerErrorKind> {
                self.inc(Call::SetWSize);
                Ok(())
            }

            async fn execute(&mut self, _: &str, _: Vec<Json>) -> Result<Json, RunnerErrorKind> {
                self.inc(Call::Exec);
                Ok(Json::Null)
            }

            async fn execute_async(
                &mut self,
                _: &str,
                _: Vec<Json>,
            ) -> Result<Json, RunnerErrorKind> {
                self.inc(Call::ExecAsync);
                Ok(Json::Null)
            }

            async fn close(&mut self) -> Result<(), RunnerErrorKind> {
                self.inc(Call::Close);
                Ok(())
            }

            async fn alert_text(&mut self) -> Result<String, RunnerErrorKind> {
                self.inc(Call::AlertText);
                Ok("".to_string())
            }

            async fn alert_accept(&mut self) -> Result<(), RunnerErrorKind> {
                self.inc(Call::AlertAccept);
                Ok(())
            }

            async fn alert_dissmis(&mut self) -> Result<(), RunnerErrorKind> {
                self.inc(Call::AlertDissmis);
                Ok(())
            }

            async fn double_click(&mut self, _: Locator) -> Result<(), RunnerErrorKind> {
                self.inc(Call::DoubleClick);
                Ok(())
            }

            async fn mouse_down(&mut self, _: Locator) -> Result<(), RunnerErrorKind> {
                self.inc(Call::MouseDown);
                Ok(())
            }

            async fn mouse_up(&mut self, _: Locator) -> Result<(), RunnerErrorKind> {
                self.inc(Call::MouseUp);
                Ok(())
            }

            async fn title(&mut self) -> Result<String, RunnerErrorKind> {
                self.inc(Call::Title);
                Ok(String::new())
            }
        }

        pub struct Element(Arc<Client>);

        impl Element {
            pub fn inc(&self, call: Call) {
                self.0.inc(call);
            }
        }

        #[async_trait::async_trait]
        impl WebElement for Element {
            type Driver = Arc<Client>;

            async fn attr(&mut self, _: &str) -> Result<Option<String>, RunnerErrorKind> {
                self.inc(Call::Attr);
                Ok(None)
            }

            async fn prop(&mut self, _: &str) -> Result<Option<String>, RunnerErrorKind> {
                self.inc(Call::Prop);
                Ok(None)
            }

            async fn text(&mut self) -> Result<String, RunnerErrorKind> {
                self.inc(Call::Text);
                Ok("".to_string())
            }

            async fn html(&mut self, _: bool) -> Result<String, RunnerErrorKind> {
                self.inc(Call::Html);
                Ok("".to_string())
            }

            async fn find(&mut self, _: Locator) -> Result<Self, RunnerErrorKind>
            where
                Self: Sized,
            {
                self.inc(Call::Find);
                Ok(Element(self.0.clone()))
            }

            async fn click(mut self) -> Result<Self::Driver, RunnerErrorKind> {
                self.inc(Call::Click);
                Ok(self.0)
            }

            async fn select_by_index(mut self, _: usize) -> Result<Self::Driver, RunnerErrorKind> {
                self.inc(Call::SelectByIndex);
                Ok(self.0)
            }

            async fn select_by_value(mut self, _: &str) -> Result<Self::Driver, RunnerErrorKind> {
                self.inc(Call::SelectByValue);
                Ok(self.0)
            }

            async fn send_keys(mut self, _: &str) -> Result<(), RunnerErrorKind> {
                self.inc(Call::SendKeys);
                Ok(())
            }

            async fn select_by_label(mut self, _: &str) -> Result<Self::Driver, RunnerErrorKind> {
                self.inc(Call::SelectByLabel);
                Ok(self.0)
            }

            async fn is_selected(&mut self) -> Result<bool, RunnerErrorKind> {
                self.inc(Call::IsSelected);
                Ok(true)
            }

            async fn is_present(&mut self) -> Result<bool, RunnerErrorKind> {
                self.inc(Call::IsPresent);
                Ok(true)
            }

            async fn is_enabled(&mut self) -> Result<bool, RunnerErrorKind> {
                self.inc(Call::IsEnabled);
                Ok(true)
            }
        }

        #[derive(Clone, Default)]
        pub struct CallCount {
            inner: std::collections::HashMap<Call, usize>,
        }

        #[derive(Hash, PartialEq, Eq, Clone)]
        pub enum Call {
            Open,
            Click,
            Find,
            FindAll,
            Goto,
            Exec,
            ExecAsync,
            Close,
            CurrentUrl,
            SetWSize,
            W8Visib,
            W8NotVisib,
            W8Pres,
            W8NPres,
            W8Edit,
            W8NotEdit,
            Attr,
            Prop,
            Text,
            Html,
            SelectByIndex,
            SelectByValue,
            SelectByLabel,
            AlertText,
            AlertAccept,
            AlertDissmis,
            DoubleClick,
            SendKeys,
            MouseDown,
            MouseUp,
            Title,
            IsSelected,
            IsPresent,
            IsEnabled,
        }

        impl Index<Call> for CallCount {
            type Output = usize;

            fn index(&self, call: Call) -> &Self::Output {
                self.inner.get(&call).unwrap()
            }
        }

        impl IndexMut<Call> for CallCount {
            fn index_mut(&mut self, call: Call) -> &mut Self::Output {
                self.inner.entry(call).or_default()
            }
        }
    }
}

fn blank_cmd(cmd: Cmd) -> Command {
    Command::new("".to_owned(), "".to_owned(), cmd)
}