scout_json/
lib.rs

1use scout_parser::ast::{CallLiteral, ExprKind, Identifier, Program, StmtKind};
2use serde::Deserialize;
3
4/// ScoutJson is a JSON representation of a subset of the Scout AST.
5/// It is meant to model after the Google Chrome Recorder API.
6#[derive(Debug, Deserialize)]
7pub struct ScoutJSON {
8    steps: Vec<Step>,
9}
10
11#[derive(Debug, Deserialize)]
12#[serde(tag = "type")]
13#[serde(rename_all = "camelCase")]
14pub enum Step {
15    SetViewport {
16        width: u32,
17        height: u32,
18    },
19    Navigate {
20        url: String,
21    },
22    Click {
23        selectors: Vec<Vec<String>>,
24    },
25    Change {
26        value: String,
27        selectors: Vec<Vec<String>>,
28    },
29}
30
31impl ScoutJSON {
32    pub fn to_ast(&self) -> Program {
33        let mut stmts = Vec::new();
34        for step in &self.steps {
35            stmts.push(step.to_stmt());
36        }
37
38        Program { stmts }
39    }
40}
41
42impl Step {
43    pub fn to_stmt(&self) -> StmtKind {
44        use Step::*;
45        match self {
46            SetViewport { width, height } => {
47                let lit = CallLiteral {
48                    ident: Identifier::new("setViewport".to_string()),
49                    args: vec![
50                        ExprKind::Number(*width as f64),
51                        ExprKind::Number(*height as f64),
52                    ],
53                    kwargs: Vec::new(),
54                };
55                StmtKind::Expr(ExprKind::Call(lit))
56            }
57            Navigate { url } => StmtKind::Goto(ExprKind::Str(url.clone())),
58            Click { selectors } => {
59                let elem = ExprKind::Select(selector_from_recorder_mtx(selectors.as_ref()), None);
60                let lit = CallLiteral {
61                    ident: Identifier::new("click".to_string()),
62                    args: vec![elem],
63                    kwargs: Vec::new(),
64                };
65                StmtKind::Expr(ExprKind::Call(lit))
66            }
67            Change { value, selectors } => {
68                let elem = ExprKind::Select(selector_from_recorder_mtx(selectors.as_ref()), None);
69                let val = ExprKind::Str(value.clone());
70                let lit = CallLiteral {
71                    ident: Identifier::new("input".to_string()),
72                    args: vec![elem, val],
73                    kwargs: Vec::new(),
74                };
75                StmtKind::Expr(ExprKind::Call(lit))
76            }
77        }
78    }
79}
80
81fn selector_from_recorder_mtx(mtx: &[Vec<String>]) -> String {
82    // By default, chrome outputs an arry and the length depends upon what
83    // outputs are set in the recording. We will assume only CSS is set as
84    // the others are not usable by scout yet.
85    // The css value is an array of length 1, ex:
86    //
87    // "selectors": [
88    //     [
89    //         "#question-summary-78853169 h3 > a"
90    //     ]
91    // ]
92    mtx[0][0].clone()
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use test_case::test_case;
99
100    #[test_case(
101        r#"{
102            "type": "navigate",
103            "url": "https://stackoverflow.com/",
104            "assertedEvents": [
105                {
106                    "type": "navigation",
107                    "url": "https://stackoverflow.com/",
108                    "title": ""
109                }
110            ]
111        }"#,
112        StmtKind::Goto(ExprKind::Str("https://stackoverflow.com/".to_string()));
113        "navigate step"
114    )]
115    #[test_case(
116        r##"{
117            "type": "click",
118            "target": "main",
119            "selectors": [
120                [
121                    "#question-summary-78853169 h3 > a"
122                ]
123            ],
124            "offsetY": 2.875,
125            "offsetX": 183,
126            "assertedEvents": [
127                {
128                    "type": "navigation",
129                    "url": "https://stackoverflow.com/questions/78853169/how-can-i-pass-variables-to-svelte-through-csv",
130                    "title": "typescript - How can I pass variables to svelte through CSV - Stack Overflow"
131                }
132            ]
133        }"##,
134        StmtKind::Expr(ExprKind::Call(CallLiteral {
135            ident: Identifier::new("click".to_string()),
136            args: vec![ExprKind::Select("#question-summary-78853169 h3 > a".to_string(), None)],
137            kwargs: Vec::new(),
138        }));
139        "click step"
140    )]
141    #[test_case(
142        r#"{
143            "type": "setViewport",
144            "width": 1365,
145            "height": 945,
146            "deviceScaleFactor": 1,
147            "isMobile": false,
148            "hasTouch": false,
149            "isLandscape": false
150        }"#,
151        StmtKind::Expr(ExprKind::Call(CallLiteral {
152            ident: Identifier::new("setViewport".to_string()),
153            args: vec![
154                ExprKind::Number(1365.),
155                ExprKind::Number(945.),
156            ],
157            kwargs: Vec::new(),
158        }));
159        "setViewport step"
160    )]
161    fn parse_step_json(input: &str, exp: StmtKind) {
162        assert_eq!(exp, serde_json::from_str::<Step>(input).unwrap().to_stmt())
163    }
164}