webdriver/
capabilities.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::common::MAX_SAFE_INTEGER;
6use crate::error::{ErrorStatus, WebDriverError, WebDriverResult};
7use serde_json::{Map, Value};
8use url::Url;
9
10pub type Capabilities = Map<String, Value>;
11
12/// Trait for objects that can be used to inspect browser capabilities
13///
14/// The main methods in this trait are called with a Capabilites object
15/// resulting from a full set of potential capabilites for the session.  Given
16/// those Capabilities they return a property of the browser instance that
17/// would be initiated. In many cases this will be independent of the input,
18/// but in the case of e.g. browser version, it might depend on a path to the
19/// binary provided as a capability.
20pub trait BrowserCapabilities {
21    /// Set up the Capabilites object
22    ///
23    /// Typically used to create any internal caches
24    fn init(&mut self, _: &Capabilities);
25
26    /// Name of the browser
27    fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
28
29    /// Version number of the browser
30    fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
31
32    /// Compare actual browser version to that provided in a version specifier
33    ///
34    /// Parameters are the actual browser version and the comparison string,
35    /// respectively. The format of the comparison string is
36    /// implementation-defined.
37    fn compare_browser_version(&mut self, version: &str, comparison: &str)
38        -> WebDriverResult<bool>;
39
40    /// Name of the platform/OS
41    fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
42
43    /// Whether insecure certificates are supported
44    fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
45
46    /// Indicates whether driver supports all of the window resizing and
47    /// repositioning commands.
48    fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
49
50    /// Indicates that interactability checks will be applied to `<input type=file>`.
51    fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
52
53    /// Whether a WebSocket URL for the created session has to be returned
54    fn web_socket_url(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
55
56    /// Indicates whether the endpoint node supports all Virtual Authenticators commands.
57    fn webauthn_virtual_authenticators(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
58
59    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the User
60    /// Verification Method extension.
61    fn webauthn_extension_uvm(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
62
63    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the prf
64    /// extension.
65    fn webauthn_extension_prf(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
66
67    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the
68    /// largeBlob extension.
69    fn webauthn_extension_large_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
70
71    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the credBlob
72    /// extension.
73    fn webauthn_extension_cred_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
74
75    fn accept_proxy(
76        &mut self,
77        proxy_settings: &Map<String, Value>,
78        _: &Capabilities,
79    ) -> WebDriverResult<bool>;
80
81    /// Type check custom properties
82    ///
83    /// Check that custom properties containing ":" have the correct data types.
84    /// Properties that are unrecognised must be ignored i.e. return without
85    /// error.
86    fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()>;
87
88    /// Check if custom properties are accepted capabilites
89    ///
90    /// Check that custom properties containing ":" are compatible with
91    /// the implementation.
92    fn accept_custom(
93        &mut self,
94        name: &str,
95        value: &Value,
96        merged: &Capabilities,
97    ) -> WebDriverResult<bool>;
98}
99
100/// Trait to abstract over various version of the new session parameters
101///
102/// This trait is expected to be implemented on objects holding the capabilities
103/// from a new session command.
104pub trait CapabilitiesMatching {
105    /// Match the BrowserCapabilities against some candidate capabilites
106    ///
107    /// Takes a BrowserCapabilites object and returns a set of capabilites that
108    /// are valid for that browser, if any, or None if there are no matching
109    /// capabilities.
110    fn match_browser<T: BrowserCapabilities>(
111        &self,
112        browser_capabilities: &mut T,
113    ) -> WebDriverResult<Option<Capabilities>>;
114}
115
116#[derive(Debug, PartialEq, Serialize, Deserialize)]
117pub struct SpecNewSessionParameters {
118    #[serde(default = "Capabilities::default")]
119    pub alwaysMatch: Capabilities,
120    #[serde(default = "firstMatch_default")]
121    pub firstMatch: Vec<Capabilities>,
122}
123
124impl Default for SpecNewSessionParameters {
125    fn default() -> Self {
126        SpecNewSessionParameters {
127            alwaysMatch: Capabilities::new(),
128            firstMatch: vec![Capabilities::new()],
129        }
130    }
131}
132
133fn firstMatch_default() -> Vec<Capabilities> {
134    vec![Capabilities::default()]
135}
136
137impl SpecNewSessionParameters {
138    fn validate<T: BrowserCapabilities>(
139        &self,
140        mut capabilities: Capabilities,
141        browser_capabilities: &mut T,
142    ) -> WebDriverResult<Capabilities> {
143        // Filter out entries with the value `null`
144        let null_entries = capabilities
145            .iter()
146            .filter(|&(_, value)| *value == Value::Null)
147            .map(|(k, _)| k.clone())
148            .collect::<Vec<String>>();
149        for key in null_entries {
150            capabilities.remove(&key);
151        }
152
153        for (key, value) in &capabilities {
154            match &**key {
155                x @ "acceptInsecureCerts"
156                | x @ "setWindowRect"
157                | x @ "strictFileInteractability"
158                | x @ "webSocketUrl"
159                | x @ "webauthn:virtualAuthenticators"
160                | x @ "webauthn:extension:uvm"
161                | x @ "webauthn:extension:prf"
162                | x @ "webauthn:extension:largeBlob"
163                | x @ "webauthn:extension:credBlob" => {
164                    if !value.is_boolean() {
165                        return Err(WebDriverError::new(
166                            ErrorStatus::InvalidArgument,
167                            format!("{} is not boolean: {}", x, value),
168                        ));
169                    }
170                }
171                x @ "browserName" | x @ "browserVersion" | x @ "platformName" => {
172                    if !value.is_string() {
173                        return Err(WebDriverError::new(
174                            ErrorStatus::InvalidArgument,
175                            format!("{} is not a string: {}", x, value),
176                        ));
177                    }
178                }
179                "pageLoadStrategy" => SpecNewSessionParameters::validate_page_load_strategy(value)?,
180                "proxy" => SpecNewSessionParameters::validate_proxy(value)?,
181                "timeouts" => SpecNewSessionParameters::validate_timeouts(value)?,
182                "unhandledPromptBehavior" => {
183                    SpecNewSessionParameters::validate_unhandled_prompt_behavior(value)?
184                }
185                x => {
186                    if !x.contains(':') {
187                        return Err(WebDriverError::new(
188                            ErrorStatus::InvalidArgument,
189                            format!(
190                                "{} is not the name of a known capability or extension capability",
191                                x
192                            ),
193                        ));
194                    } else {
195                        browser_capabilities.validate_custom(x, value)?
196                    }
197                }
198            }
199        }
200
201        // With a value of `false` the capability needs to be removed.
202        if let Some(Value::Bool(false)) = capabilities.get(&"webSocketUrl".to_string()) {
203            capabilities.remove(&"webSocketUrl".to_string());
204        }
205
206        Ok(capabilities)
207    }
208
209    fn validate_page_load_strategy(value: &Value) -> WebDriverResult<()> {
210        match value {
211            Value::String(x) => match &**x {
212                "normal" | "eager" | "none" => {}
213                x => {
214                    return Err(WebDriverError::new(
215                        ErrorStatus::InvalidArgument,
216                        format!("Invalid page load strategy: {}", x),
217                    ))
218                }
219            },
220            _ => {
221                return Err(WebDriverError::new(
222                    ErrorStatus::InvalidArgument,
223                    "pageLoadStrategy is not a string",
224                ))
225            }
226        }
227        Ok(())
228    }
229
230    fn validate_proxy(proxy_value: &Value) -> WebDriverResult<()> {
231        let obj = try_opt!(
232            proxy_value.as_object(),
233            ErrorStatus::InvalidArgument,
234            "proxy is not an object"
235        );
236
237        for (key, value) in obj {
238            match &**key {
239                "proxyType" => match value.as_str() {
240                    Some("pac") | Some("direct") | Some("autodetect") | Some("system")
241                    | Some("manual") => {}
242                    Some(x) => {
243                        return Err(WebDriverError::new(
244                            ErrorStatus::InvalidArgument,
245                            format!("Invalid proxyType value: {}", x),
246                        ))
247                    }
248                    None => {
249                        return Err(WebDriverError::new(
250                            ErrorStatus::InvalidArgument,
251                            format!("proxyType is not a string: {}", value),
252                        ))
253                    }
254                },
255
256                "proxyAutoconfigUrl" => match value.as_str() {
257                    Some(x) => {
258                        Url::parse(x).map_err(|_| {
259                            WebDriverError::new(
260                                ErrorStatus::InvalidArgument,
261                                format!("proxyAutoconfigUrl is not a valid URL: {}", x),
262                            )
263                        })?;
264                    }
265                    None => {
266                        return Err(WebDriverError::new(
267                            ErrorStatus::InvalidArgument,
268                            "proxyAutoconfigUrl is not a string",
269                        ))
270                    }
271                },
272
273                "ftpProxy" => SpecNewSessionParameters::validate_host(value, "ftpProxy")?,
274                "httpProxy" => SpecNewSessionParameters::validate_host(value, "httpProxy")?,
275                "noProxy" => SpecNewSessionParameters::validate_no_proxy(value)?,
276                "sslProxy" => SpecNewSessionParameters::validate_host(value, "sslProxy")?,
277                "socksProxy" => SpecNewSessionParameters::validate_host(value, "socksProxy")?,
278                "socksVersion" => {
279                    if !value.is_number() {
280                        return Err(WebDriverError::new(
281                            ErrorStatus::InvalidArgument,
282                            format!("socksVersion is not a number: {}", value),
283                        ));
284                    }
285                }
286
287                x => {
288                    return Err(WebDriverError::new(
289                        ErrorStatus::InvalidArgument,
290                        format!("Invalid proxy configuration entry: {}", x),
291                    ))
292                }
293            }
294        }
295
296        Ok(())
297    }
298
299    fn validate_no_proxy(value: &Value) -> WebDriverResult<()> {
300        match value.as_array() {
301            Some(hosts) => {
302                for host in hosts {
303                    match host.as_str() {
304                        Some(_) => {}
305                        None => {
306                            return Err(WebDriverError::new(
307                                ErrorStatus::InvalidArgument,
308                                format!("noProxy item is not a string: {}", host),
309                            ))
310                        }
311                    }
312                }
313            }
314            None => {
315                return Err(WebDriverError::new(
316                    ErrorStatus::InvalidArgument,
317                    format!("noProxy is not an array: {}", value),
318                ))
319            }
320        }
321
322        Ok(())
323    }
324
325    /// Validate whether a named capability is JSON value is a string
326    /// containing a host and possible port
327    fn validate_host(value: &Value, entry: &str) -> WebDriverResult<()> {
328        match value.as_str() {
329            Some(host) => {
330                if host.contains("://") {
331                    return Err(WebDriverError::new(
332                        ErrorStatus::InvalidArgument,
333                        format!("{} must not contain a scheme: {}", entry, host),
334                    ));
335                }
336
337                // Temporarily add a scheme so the host can be parsed as URL
338                let url = Url::parse(&format!("http://{}", host)).map_err(|_| {
339                    WebDriverError::new(
340                        ErrorStatus::InvalidArgument,
341                        format!("{} is not a valid URL: {}", entry, host),
342                    )
343                })?;
344
345                if url.username() != ""
346                    || url.password().is_some()
347                    || url.path() != "/"
348                    || url.query().is_some()
349                    || url.fragment().is_some()
350                {
351                    return Err(WebDriverError::new(
352                        ErrorStatus::InvalidArgument,
353                        format!("{} is not of the form host[:port]: {}", entry, host),
354                    ));
355                }
356            }
357
358            None => {
359                return Err(WebDriverError::new(
360                    ErrorStatus::InvalidArgument,
361                    format!("{} is not a string: {}", entry, value),
362                ))
363            }
364        }
365
366        Ok(())
367    }
368
369    fn validate_timeouts(value: &Value) -> WebDriverResult<()> {
370        let obj = try_opt!(
371            value.as_object(),
372            ErrorStatus::InvalidArgument,
373            "timeouts capability is not an object"
374        );
375
376        for (key, value) in obj {
377            match &**key {
378                _x @ "script" if value.is_null() => {}
379
380                x @ "script" | x @ "pageLoad" | x @ "implicit" => {
381                    let timeout = try_opt!(
382                        value.as_f64(),
383                        ErrorStatus::InvalidArgument,
384                        format!("{} timeouts value is not a number: {}", x, value)
385                    );
386                    if timeout < 0.0 || timeout.fract() != 0.0 {
387                        return Err(WebDriverError::new(
388                            ErrorStatus::InvalidArgument,
389                            format!(
390                                "'{}' timeouts value is not a positive Integer: {}",
391                                x, timeout
392                            ),
393                        ));
394                    }
395                    if (timeout as u64) > MAX_SAFE_INTEGER {
396                        return Err(WebDriverError::new(
397                            ErrorStatus::InvalidArgument,
398                            format!(
399                                "'{}' timeouts value is greater than maximum safe integer: {}",
400                                x, timeout
401                            ),
402                        ));
403                    }
404                }
405
406                x => {
407                    return Err(WebDriverError::new(
408                        ErrorStatus::InvalidArgument,
409                        format!("Invalid timeouts capability entry: {}", x),
410                    ))
411                }
412            }
413        }
414
415        Ok(())
416    }
417
418    fn validate_unhandled_prompt_behavior(value: &Value) -> WebDriverResult<()> {
419        match value {
420            Value::Object(obj) => {
421                // Unhandled Prompt Behavior type as used by WebDriver BiDi
422                for (key, value) in obj {
423                    match &**key {
424                        x @ "alert"
425                        | x @ "beforeUnload"
426                        | x @ "confirm"
427                        | x @ "default"
428                        | x @ "prompt" => {
429                            let behavior = try_opt!(
430                                value.as_str(),
431                                ErrorStatus::InvalidArgument,
432                                format!(
433                                    "'{}' unhandledPromptBehavior value is not a string: {}",
434                                    x, value
435                                )
436                            );
437
438                            match behavior {
439                                "accept" | "accept and notify" | "dismiss"
440                                | "dismiss and notify" | "ignore" => {}
441                                x => {
442                                    return Err(WebDriverError::new(
443                                        ErrorStatus::InvalidArgument,
444                                        format!(
445                                            "'{}' unhandledPromptBehavior value is invalid: {}",
446                                            x, behavior
447                                        ),
448                                    ))
449                                }
450                            }
451                        }
452                        x => {
453                            return Err(WebDriverError::new(
454                                ErrorStatus::InvalidArgument,
455                                format!("Invalid unhandledPromptBehavior entry: {}", x),
456                            ))
457                        }
458                    }
459                }
460            }
461            Value::String(behavior) => match behavior.as_str() {
462                "accept" | "accept and notify" | "dismiss" | "dismiss and notify" | "ignore" => {}
463                x => {
464                    return Err(WebDriverError::new(
465                        ErrorStatus::InvalidArgument,
466                        format!("Invalid unhandledPromptBehavior value: {}", x),
467                    ))
468                }
469            },
470            _ => {
471                return Err(WebDriverError::new(
472                    ErrorStatus::InvalidArgument,
473                    format!(
474                        "unhandledPromptBehavior is neither an object nor a string: {}",
475                        value
476                    ),
477                ))
478            }
479        }
480
481        Ok(())
482    }
483}
484
485impl CapabilitiesMatching for SpecNewSessionParameters {
486    fn match_browser<T: BrowserCapabilities>(
487        &self,
488        browser_capabilities: &mut T,
489    ) -> WebDriverResult<Option<Capabilities>> {
490        let default = vec![Map::new()];
491        let capabilities_list = if self.firstMatch.is_empty() {
492            &default
493        } else {
494            &self.firstMatch
495        };
496
497        let merged_capabilities = capabilities_list
498            .iter()
499            .map(|first_match_entry| {
500                if first_match_entry
501                    .keys()
502                    .any(|k| self.alwaysMatch.contains_key(k))
503                {
504                    return Err(WebDriverError::new(
505                        ErrorStatus::InvalidArgument,
506                        "firstMatch key shadowed a value in alwaysMatch",
507                    ));
508                }
509                let mut merged = self.alwaysMatch.clone();
510                for (key, value) in first_match_entry.clone() {
511                    merged.insert(key, value);
512                }
513                Ok(merged)
514            })
515            .map(|merged| merged.and_then(|x| self.validate(x, browser_capabilities)))
516            .collect::<WebDriverResult<Vec<Capabilities>>>()?;
517
518        let selected = merged_capabilities
519            .iter()
520            .find(|merged| {
521                browser_capabilities.init(merged);
522
523                for (key, value) in merged.iter() {
524                    match &**key {
525                        "browserName" => {
526                            let browserValue = browser_capabilities
527                                .browser_name(merged)
528                                .ok()
529                                .and_then(|x| x);
530
531                            if value.as_str() != browserValue.as_deref() {
532                                return false;
533                            }
534                        }
535                        "browserVersion" => {
536                            let browserValue = browser_capabilities
537                                .browser_version(merged)
538                                .ok()
539                                .and_then(|x| x);
540                            // We already validated this was a string
541                            let version_cond = value.as_str().unwrap_or("");
542                            if let Some(version) = browserValue {
543                                if !browser_capabilities
544                                    .compare_browser_version(&version, version_cond)
545                                    .unwrap_or(false)
546                                {
547                                    return false;
548                                }
549                            } else {
550                                return false;
551                            }
552                        }
553                        "platformName" => {
554                            let browserValue = browser_capabilities
555                                .platform_name(merged)
556                                .ok()
557                                .and_then(|x| x);
558                            if value.as_str() != browserValue.as_deref() {
559                                return false;
560                            }
561                        }
562                        "acceptInsecureCerts" => {
563                            if value.as_bool().unwrap_or(false)
564                                && !browser_capabilities
565                                    .accept_insecure_certs(merged)
566                                    .unwrap_or(false)
567                            {
568                                return false;
569                            }
570                        }
571                        "setWindowRect" => {
572                            if value.as_bool().unwrap_or(false)
573                                && !browser_capabilities
574                                    .set_window_rect(merged)
575                                    .unwrap_or(false)
576                            {
577                                return false;
578                            }
579                        }
580                        "strictFileInteractability" => {
581                            if value.as_bool().unwrap_or(false)
582                                && !browser_capabilities
583                                    .strict_file_interactability(merged)
584                                    .unwrap_or(false)
585                            {
586                                return false;
587                            }
588                        }
589                        "proxy" => {
590                            let default = Map::new();
591                            let proxy = value.as_object().unwrap_or(&default);
592                            if !browser_capabilities
593                                .accept_proxy(proxy, merged)
594                                .unwrap_or(false)
595                            {
596                                return false;
597                            }
598                        }
599                        "webSocketUrl" => {
600                            if value.as_bool().unwrap_or(false)
601                                && !browser_capabilities.web_socket_url(merged).unwrap_or(false)
602                            {
603                                return false;
604                            }
605                        }
606                        "webauthn:virtualAuthenticators" => {
607                            if value.as_bool().unwrap_or(false)
608                                && !browser_capabilities
609                                    .webauthn_virtual_authenticators(merged)
610                                    .unwrap_or(false)
611                            {
612                                return false;
613                            }
614                        }
615                        "webauthn:extension:uvm" => {
616                            if value.as_bool().unwrap_or(false)
617                                && !browser_capabilities
618                                    .webauthn_extension_uvm(merged)
619                                    .unwrap_or(false)
620                            {
621                                return false;
622                            }
623                        }
624                        "webauthn:extension:prf" => {
625                            if value.as_bool().unwrap_or(false)
626                                && !browser_capabilities
627                                    .webauthn_extension_prf(merged)
628                                    .unwrap_or(false)
629                            {
630                                return false;
631                            }
632                        }
633                        "webauthn:extension:largeBlob" => {
634                            if value.as_bool().unwrap_or(false)
635                                && !browser_capabilities
636                                    .webauthn_extension_large_blob(merged)
637                                    .unwrap_or(false)
638                            {
639                                return false;
640                            }
641                        }
642                        "webauthn:extension:credBlob" => {
643                            if value.as_bool().unwrap_or(false)
644                                && !browser_capabilities
645                                    .webauthn_extension_cred_blob(merged)
646                                    .unwrap_or(false)
647                            {
648                                return false;
649                            }
650                        }
651                        name => {
652                            if name.contains(':') {
653                                if !browser_capabilities
654                                    .accept_custom(name, value, merged)
655                                    .unwrap_or(false)
656                                {
657                                    return false;
658                                }
659                            } else {
660                                // Accept the capability
661                            }
662                        }
663                    }
664                }
665
666                true
667            })
668            .cloned();
669        Ok(selected)
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use crate::test::assert_de;
677    use serde_json::{self, json};
678
679    #[test]
680    fn test_json_spec_new_session_parameters_alwaysMatch_only() {
681        let caps = SpecNewSessionParameters {
682            alwaysMatch: Capabilities::new(),
683            firstMatch: vec![Capabilities::new()],
684        };
685        assert_de(&caps, json!({"alwaysMatch": {}}));
686    }
687
688    #[test]
689    fn test_json_spec_new_session_parameters_firstMatch_only() {
690        let caps = SpecNewSessionParameters {
691            alwaysMatch: Capabilities::new(),
692            firstMatch: vec![Capabilities::new()],
693        };
694        assert_de(&caps, json!({"firstMatch": [{}]}));
695    }
696
697    #[test]
698    fn test_json_spec_new_session_parameters_alwaysMatch_null() {
699        let json = json!({
700            "alwaysMatch": null,
701            "firstMatch": [{}],
702        });
703        assert!(serde_json::from_value::<SpecNewSessionParameters>(json).is_err());
704    }
705
706    #[test]
707    fn test_json_spec_new_session_parameters_firstMatch_null() {
708        let json = json!({
709            "alwaysMatch": {},
710            "firstMatch": null,
711        });
712        assert!(serde_json::from_value::<SpecNewSessionParameters>(json).is_err());
713    }
714
715    #[test]
716    fn test_json_spec_new_session_parameters_both_empty() {
717        let json = json!({
718            "alwaysMatch": {},
719            "firstMatch": [{}],
720        });
721        let caps = SpecNewSessionParameters {
722            alwaysMatch: Capabilities::new(),
723            firstMatch: vec![Capabilities::new()],
724        };
725
726        assert_de(&caps, json);
727    }
728
729    #[test]
730    fn test_json_spec_new_session_parameters_both_with_capability() {
731        let json = json!({
732            "alwaysMatch": {"foo": "bar"},
733            "firstMatch": [{"foo2": "bar2"}],
734        });
735        let mut caps = SpecNewSessionParameters {
736            alwaysMatch: Capabilities::new(),
737            firstMatch: vec![Capabilities::new()],
738        };
739        caps.alwaysMatch.insert("foo".into(), "bar".into());
740        caps.firstMatch[0].insert("foo2".into(), "bar2".into());
741
742        assert_de(&caps, json);
743    }
744
745    #[test]
746    fn test_validate_unhandled_prompt_behavior() {
747        fn validate_prompt_behavior(v: Value) -> WebDriverResult<()> {
748            SpecNewSessionParameters::validate_unhandled_prompt_behavior(&v)
749        }
750
751        // capability as string
752        validate_prompt_behavior(json!("accept")).unwrap();
753        validate_prompt_behavior(json!("accept and notify")).unwrap();
754        validate_prompt_behavior(json!("dismiss")).unwrap();
755        validate_prompt_behavior(json!("dismiss and notify")).unwrap();
756        validate_prompt_behavior(json!("ignore")).unwrap();
757        assert!(validate_prompt_behavior(json!("foo")).is_err());
758
759        // capability as object
760        let types = ["alert", "beforeUnload", "confirm", "default", "prompt"];
761        let handlers = [
762            "accept",
763            "accept and notify",
764            "dismiss",
765            "dismiss and notify",
766            "ignore",
767        ];
768        for promptType in types {
769            assert!(validate_prompt_behavior(json!({promptType: "foo"})).is_err());
770            for handler in handlers {
771                validate_prompt_behavior(json!({promptType: handler})).unwrap();
772            }
773        }
774
775        for handler in handlers {
776            assert!(validate_prompt_behavior(json!({"foo": handler})).is_err());
777        }
778    }
779
780    #[test]
781    fn test_validate_proxy() {
782        fn validate_proxy(v: Value) -> WebDriverResult<()> {
783            SpecNewSessionParameters::validate_proxy(&v)
784        }
785
786        // proxy hosts
787        validate_proxy(json!({"httpProxy":  "127.0.0.1"})).unwrap();
788        validate_proxy(json!({"httpProxy": "127.0.0.1:"})).unwrap();
789        validate_proxy(json!({"httpProxy": "127.0.0.1:3128"})).unwrap();
790        validate_proxy(json!({"httpProxy": "localhost"})).unwrap();
791        validate_proxy(json!({"httpProxy": "localhost:3128"})).unwrap();
792        validate_proxy(json!({"httpProxy": "[2001:db8::1]"})).unwrap();
793        validate_proxy(json!({"httpProxy": "[2001:db8::1]:3128"})).unwrap();
794        validate_proxy(json!({"httpProxy": "example.org"})).unwrap();
795        validate_proxy(json!({"httpProxy": "example.org:3128"})).unwrap();
796
797        assert!(validate_proxy(json!({"httpProxy": "http://example.org"})).is_err());
798        assert!(validate_proxy(json!({"httpProxy": "example.org:-1"})).is_err());
799        assert!(validate_proxy(json!({"httpProxy": "2001:db8::1"})).is_err());
800
801        // no proxy for manual proxy type
802        validate_proxy(json!({"noProxy": ["foo"]})).unwrap();
803
804        assert!(validate_proxy(json!({"noProxy": "foo"})).is_err());
805        assert!(validate_proxy(json!({"noProxy": [42]})).is_err());
806    }
807}