1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use scout_parser::ast::{CallLiteral, ExprKind, Identifier, Program, StmtKind};
use serde::Deserialize;

/// ScoutJson is a JSON representation of a subset of the Scout AST.
/// It is meant to model after the Google Chrome Recorder API.
#[derive(Debug, Deserialize)]
pub struct ScoutJSON {
    steps: Vec<Step>,
}

#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum Step {
    SetViewport { width: u32, height: u32 },
    Navigate { url: String },
    Click { selectors: Vec<Vec<String>> },
}

impl ScoutJSON {
    pub fn to_ast(&self) -> Program {
        let mut stmts = Vec::new();
        for step in &self.steps {
            stmts.push(step.to_stmt());
        }

        Program { stmts }
    }
}

impl Step {
    pub fn to_stmt(&self) -> StmtKind {
        use Step::*;
        match self {
            SetViewport { width, height } => {
                let lit = CallLiteral {
                    ident: Identifier::new("setViewport".to_string()),
                    args: vec![
                        ExprKind::Number(*width as f64),
                        ExprKind::Number(*height as f64),
                    ],
                    kwargs: Vec::new(),
                };
                StmtKind::Expr(ExprKind::Call(lit))
            }
            Navigate { url } => StmtKind::Goto(ExprKind::Str(url.clone())),
            Click { selectors } => {
                // By default, chrome outputs an arry and the length depends upon what
                // outputs are set in the recording. We will assume only CSS is set as
                // the others are not usable by scout yet.
                // The css value is an array of length 1, ex:
                //
                // "selectors": [
                //     [
                //         "#question-summary-78853169 h3 > a"
                //     ]
                // ]
                let elem = ExprKind::Select(selectors[0][0].clone(), None);
                let lit = CallLiteral {
                    ident: Identifier::new("click".to_string()),
                    args: vec![elem],
                    kwargs: Vec::new(),
                };
                StmtKind::Expr(ExprKind::Call(lit))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use test_case::test_case;

    #[test_case(
        r#"{
            "type": "navigate",
            "url": "https://stackoverflow.com/",
            "assertedEvents": [
                {
                    "type": "navigation",
                    "url": "https://stackoverflow.com/",
                    "title": ""
                }
            ]
        }"#,
        StmtKind::Goto(ExprKind::Str("https://stackoverflow.com/".to_string()));
        "navigate step"
    )]
    #[test_case(
        r##"{
            "type": "click",
            "target": "main",
            "selectors": [
                [
                    "#question-summary-78853169 h3 > a"
                ]
            ],
            "offsetY": 2.875,
            "offsetX": 183,
            "assertedEvents": [
                {
                    "type": "navigation",
                    "url": "https://stackoverflow.com/questions/78853169/how-can-i-pass-variables-to-svelte-through-csv",
                    "title": "typescript - How can I pass variables to svelte through CSV - Stack Overflow"
                }
            ]
        }"##,
        StmtKind::Expr(ExprKind::Call(CallLiteral {
            ident: Identifier::new("click".to_string()),
            args: vec![ExprKind::Select("#question-summary-78853169 h3 > a".to_string(), None)],
            kwargs: Vec::new(),
        }));
        "click step"
    )]
    #[test_case(
        r#"{
            "type": "setViewport",
            "width": 1365,
            "height": 945,
            "deviceScaleFactor": 1,
            "isMobile": false,
            "hasTouch": false,
            "isLandscape": false
        }"#,
        StmtKind::Expr(ExprKind::Call(CallLiteral {
            ident: Identifier::new("setViewport".to_string()),
            args: vec![
                ExprKind::Number(1365.),
                ExprKind::Number(945.),
            ],
            kwargs: Vec::new(),
        }));
        "setViewport step"
    )]
    fn parse_step_json(input: &str, exp: StmtKind) {
        assert_eq!(exp, serde_json::from_str::<Step>(input).unwrap().to_stmt())
    }
}