Skip to main content

ferrijs_std/url/
url_search_params.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use std::{
4    cell::RefCell,
5    collections::{HashMap, HashSet},
6    rc::Rc,
7};
8
9use crate::utils::{
10    bytes::get_lossy_string,
11    class::{iterator_result, live_iterator, IterKind},
12    primordials::{BasePrimordials, Primordial},
13    string::get_coerced_defined_string,
14};
15use rquickjs::{
16    atom::PredefinedAtom, class::Trace, function::Opt, prelude::This, Array, Class, Coerced, Ctx,
17    Exception, FromJs, Function, IntoJs, Null, Object, Result, Symbol, Value,
18};
19use url::Url;
20
21use super::convert_trailing_space;
22
23/// Represents `URLSearchParams` in the JavaScript context
24///
25/// <https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams>
26///
27/// # Examples
28///
29/// ```rust,ignore
30/// // This is JavaScript
31/// const params = new URLSearchParams();
32/// params.set("foo", "bar");
33/// ```
34#[derive(Clone, Trace, rquickjs::JsLifetime)]
35#[rquickjs::class]
36pub struct URLSearchParams {
37    // URL and URLSearchParams work together to manipulate URLs, so using a
38    // reference counter (Rc) allows them to have shared ownership of the
39    // undering Url, and a RefCell allows interior mutability.
40    #[qjs(skip_trace)]
41    pub url: Rc<RefCell<Url>>,
42}
43
44// URLSearchParams is designed to operate directly on the underlying Url to
45// avoid maintaining derived state that can get out of sync. When it's used
46// independently, it still needs a valid URL (http://example.com), but this
47// doesn't have any effect on using URLSearchParams with URL as the params are
48// stringified when added to a URL.
49//
50// ```js
51// const params = new URLSearchParams("foo=bar");
52// const url = new URL("http://github.com");
53// url.search = params; // This works as expected
54// ```
55#[rquickjs::methods(rename_all = "camelCase")]
56impl<'js> URLSearchParams {
57    #[qjs(constructor)]
58    pub fn new(ctx: Ctx<'js>, init: Opt<Value<'js>>) -> Result<Self> {
59        if let Some(init) = init.into_inner() {
60            if init.is_string() {
61                let string = get_lossy_string(init)?;
62                return Ok(Self::from_str(string));
63            } else if init.is_array() {
64                return Self::from_array(&ctx, unsafe { init.into_array().unwrap_unchecked() });
65            } else if init.is_object() {
66                return Self::from_object(&ctx, unsafe { init.into_object().unwrap_unchecked() });
67            } else if !init.is_undefined() {
68                // WebIDL: the init union is not nullable, so anything that
69                // is neither a sequence nor a record converts to USVString
70                // — `new URLSearchParams(null)` parses the query "null"
71                // and `new URLSearchParams(42)` the query "42", the way
72                // every browser engine does.
73                return Ok(Self::from_str(Coerced::<String>::from_js(&ctx, init)?.0));
74            }
75        }
76        let url: Url = unsafe { "http://example.com".parse().unwrap_unchecked() };
77
78        Ok(URLSearchParams {
79            url: Rc::new(RefCell::new(url)),
80        })
81    }
82
83    //
84    // Properties
85    //
86
87    #[qjs(get)]
88    pub fn size(&self) -> usize {
89        self.url.borrow().query_pairs().count()
90    }
91
92    #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
93    pub fn to_string_tag() -> &'static str {
94        stringify!(URLSearchParams)
95    }
96
97    //
98    // Instance methods
99    //
100
101    pub fn append(&mut self, key: Coerced<String>, value: Coerced<String>) {
102        convert_trailing_space(&mut self.url.borrow_mut());
103
104        self.url
105            .borrow_mut()
106            .query_pairs_mut()
107            .append_pair(key.as_str(), value.as_str());
108        self.sync_query();
109    }
110
111    pub fn delete(&mut self, key: Coerced<String>, value: Opt<Value<'js>>) {
112        convert_trailing_space(&mut self.url.borrow_mut());
113
114        let key = key.0;
115
116        let value = get_coerced_defined_string(&value.0);
117
118        let new_pairs: Vec<_> = self
119            .url
120            .borrow()
121            .query_pairs()
122            .filter(|(k, v)| {
123                if let Some(value) = value.as_ref() {
124                    return !(*k == key && *v == *value);
125                }
126                *k != key
127            })
128            .map(|(k, v)| (k.to_string(), v.to_string()))
129            .collect();
130
131        if !new_pairs.is_empty() {
132            self.url
133                .borrow_mut()
134                .query_pairs_mut()
135                .clear()
136                .extend_pairs(new_pairs);
137        } else {
138            self.url.borrow_mut().set_query(None);
139        }
140        self.sync_query();
141    }
142
143    pub fn entries(
144        this: This<Class<'js, URLSearchParams>>,
145        ctx: Ctx<'js>,
146    ) -> Result<Class<'js, URLSearchParamsIter<'js>>> {
147        URLSearchParamsIter::new(&ctx, this.0, IterKind::Entries)
148    }
149
150    pub fn for_each(
151        this: This<Class<'js, URLSearchParams>>,
152        callback: Function<'js>,
153    ) -> Result<()> {
154        // Re-read each index so the callback's mutations are observed.
155        let mut index = 0;
156        loop {
157            let pair = this
158                .0
159                .borrow()
160                .url
161                .borrow()
162                .query_pairs()
163                .nth(index)
164                .map(|(k, v)| (k.to_string(), v.to_string()));
165            let Some((k, v)) = pair else {
166                break;
167            };
168            () = callback.call((v, k, this.0.clone()))?;
169            index += 1;
170        }
171        Ok(())
172    }
173
174    pub fn get(&mut self, ctx: Ctx<'js>, key: String) -> Result<Value<'js>> {
175        match self
176            .url
177            .borrow()
178            .query_pairs()
179            .find(|(k, _)| *k == key)
180            .map(|(_, v)| v)
181        {
182            Some(value) => value.into_js(&ctx),
183            None => Null.into_js(&ctx),
184        }
185    }
186
187    pub fn get_all(&mut self, key: String) -> Vec<String> {
188        self.url
189            .borrow()
190            .query_pairs()
191            .filter_map(|(k, v)| if k == key { Some(v.to_string()) } else { None })
192            .collect()
193    }
194
195    pub fn has(&self, key: Coerced<String>, value: Opt<Value<'js>>) -> bool {
196        let value = get_coerced_defined_string(&value.0);
197        let key = key.0;
198        self.url.borrow().query_pairs().any(|(k, v)| {
199            if let Some(value) = value.as_ref() {
200                return *k == key && *v == *value;
201            }
202            *k == key
203        })
204    }
205
206    pub fn keys(
207        this: This<Class<'js, URLSearchParams>>,
208        ctx: Ctx<'js>,
209    ) -> Result<Class<'js, URLSearchParamsIter<'js>>> {
210        URLSearchParamsIter::new(&ctx, this.0, IterKind::Keys)
211    }
212
213    pub fn set(&mut self, key: Coerced<String>, value: Coerced<String>) {
214        convert_trailing_space(&mut self.url.borrow_mut());
215
216        let key = key.0;
217        let value = value.0;
218
219        // Use a HashSet just to filter duplicates
220        let mut uniques = HashSet::new();
221        let mut new_query_pairs: Vec<(String, String)> = Vec::new();
222
223        for (k, v) in self.url.borrow().query_pairs() {
224            // Update the value for an existing key
225            let value = if k == key {
226                value.clone()
227            } else {
228                v.to_string()
229            };
230
231            let query_pair = (k.to_string(), value);
232            if uniques.insert(query_pair.clone()) {
233                new_query_pairs.push(query_pair);
234            }
235        }
236
237        // Append a new key/value pair
238        let query_pair = (key, value);
239        if uniques.insert(query_pair.clone()) {
240            new_query_pairs.push(query_pair);
241        }
242
243        self.url
244            .borrow_mut()
245            .query_pairs_mut()
246            .clear()
247            .extend_pairs(new_query_pairs);
248        self.sync_query();
249    }
250
251    pub fn sort(&mut self) {
252        let mut new_pairs: Vec<(String, String)> =
253            self.url.borrow().query_pairs().into_owned().collect();
254        new_pairs.sort_by(|(a, _), (b, _)| {
255            // Spec requires sorting by UTF-16 code units
256            let a_utf16 = a.encode_utf16();
257            let b_utf16 = b.encode_utf16();
258            a_utf16.cmp(b_utf16)
259        });
260
261        if new_pairs.is_empty() {
262            self.url.borrow_mut().set_query(None);
263        } else {
264            self.url
265                .borrow_mut()
266                .query_pairs_mut()
267                .clear()
268                .extend_pairs(new_pairs);
269        }
270        self.sync_query();
271    }
272
273    pub fn to_string(&self) -> String {
274        // The Url create doesn't properly encode query params for all edge
275        // cases, so we need to construct the query string by percent-encoding
276        // each key/value
277        // TODO: This should probably be fixed in the Url crate
278        let url = self.url.borrow();
279        url.query_pairs().fold(
280            String::with_capacity(url.query().map_or(0, |q| q.len())),
281            |mut acc, (key, value)| {
282                if !acc.is_empty() {
283                    acc.push('&');
284                }
285                url::form_urlencoded::byte_serialize(key.as_bytes()).for_each(|b| acc.push_str(b));
286                acc.push('=');
287                url::form_urlencoded::byte_serialize(value.as_bytes())
288                    .for_each(|b| acc.push_str(b));
289                acc
290            },
291        )
292    }
293
294    pub fn values(
295        this: This<Class<'js, URLSearchParams>>,
296        ctx: Ctx<'js>,
297    ) -> Result<Class<'js, URLSearchParamsIter<'js>>> {
298        URLSearchParamsIter::new(&ctx, this.0, IterKind::Values)
299    }
300
301    #[qjs(rename = PredefinedAtom::SymbolIterator)]
302    pub fn iterator(
303        this: This<Class<'js, URLSearchParams>>,
304        ctx: Ctx<'js>,
305    ) -> Result<Class<'js, URLSearchParamsIter<'js>>> {
306        URLSearchParamsIter::new(&ctx, this.0, IterKind::Entries)
307    }
308}
309
310impl<'js> URLSearchParams {
311    fn read_entry(&self, index: usize, ctx: &Ctx<'js>) -> Result<Option<(Value<'js>, Value<'js>)>> {
312        let pair = self
313            .url
314            .borrow()
315            .query_pairs()
316            .nth(index)
317            .map(|(k, v)| (k.to_string(), v.to_string()));
318        match pair {
319            Some((k, v)) => Ok(Some((k.into_js(ctx)?, v.into_js(ctx)?))),
320            None => Ok(None),
321        }
322    }
323
324    /// Re-serialize the query string with proper percent-encoding.
325    /// The url crate doesn't encode commas, so we rebuild the query
326    /// using form_urlencoded::byte_serialize after each mutation.
327    fn sync_query(&self) {
328        let query = self.to_string();
329        let mut url = self.url.borrow_mut();
330        if query.is_empty() {
331            url.set_query(None);
332        } else {
333            url.set_query(Some(&query));
334        }
335    }
336
337    #[allow(clippy::should_implement_trait)]
338    pub fn from_str(query: String) -> Self {
339        let query = if !query.starts_with('?') {
340            ["?", &query].concat()
341        } else {
342            query
343        };
344        let url = unsafe {
345            "http://example.com"
346                .parse::<Url>()
347                .unwrap_unchecked()
348                .join(&query)
349                .unwrap_unchecked()
350        };
351        Self {
352            url: Rc::new(RefCell::new(url)),
353        }
354    }
355
356    pub fn from_url(url: &Rc<RefCell<Url>>) -> Self {
357        Self {
358            url: Rc::clone(url),
359        }
360    }
361
362    pub fn from_array(ctx: &Ctx<'js>, array: Array<'js>) -> Result<Self> {
363        let mut url: Url = "http://example.com".parse().unwrap();
364        let query_pairs: Vec<(String, String)> = array
365            .into_iter()
366            .map(|value| {
367                if let Ok(value) = value {
368                    if let Some(pair) = value.as_array() {
369                        if pair.len() == 2 {
370                            let key_val: Value = pair.get(0)?;
371                            let val_val: Value = pair.get(1)?;
372                            let key = if key_val.is_string() {
373                                get_lossy_string(key_val)?
374                            } else {
375                                Coerced::<String>::from_js(ctx, key_val)?.0
376                            };
377                            let value = if val_val.is_string() {
378                                get_lossy_string(val_val)?
379                            } else {
380                                Coerced::<String>::from_js(ctx, val_val)?.0
381                            };
382                            return Ok((key, value));
383                        }
384                    }
385                };
386                Err(Exception::throw_type(
387                    ctx,
388                    "Invalid tuple: Each query pair must be an iterable [name, value] tuple",
389                ))
390            })
391            .collect::<Result<Vec<_>>>()?
392            .into_iter()
393            .collect();
394
395        url.query_pairs_mut().extend_pairs(query_pairs);
396
397        Ok(Self {
398            url: Rc::new(RefCell::new(url)),
399        })
400    }
401
402    pub fn from_object(ctx: &Ctx<'js>, object: Object<'js>) -> Result<Self> {
403        let iterator = Symbol::iterator(ctx.clone());
404        if object.contains_key(iterator)? {
405            let query_pairs: Array = BasePrimordials::get(ctx)?
406                .function_array_from
407                .call((object,))?;
408            return Self::from_array(ctx, query_pairs);
409        }
410
411        let mut url: Url = "http://example.com".parse().unwrap();
412        let raw_pairs: Vec<(String, String)> = object
413            .keys::<Value<'js>>()
414            .map(|key| {
415                let key = key?;
416                let key_string = if key.is_string() {
417                    get_lossy_string(key.clone())?
418                } else {
419                    Coerced::<String>::from_js(ctx, key.clone())?.0
420                };
421                let value_val: Value = object.get(key)?;
422                let value = if value_val.is_string() {
423                    get_lossy_string(value_val)?
424                } else {
425                    Coerced::<String>::from_js(ctx, value_val)?.0
426                };
427                Ok((key_string, value))
428            })
429            .collect::<Result<Vec<_>>>()?;
430
431        // WebIDL record conversion: when multiple input keys normalise to the
432        // same string (e.g. two different lone surrogates both map to U+FFFD),
433        // the *last* value wins. Preserve original iteration order for keys
434        // that were only seen once.
435        let mut order: Vec<String> = Vec::with_capacity(raw_pairs.len());
436        let mut map: HashMap<String, String> = HashMap::with_capacity(raw_pairs.len());
437        for (k, v) in raw_pairs {
438            if !map.contains_key(&k) {
439                order.push(k.clone());
440            }
441            map.insert(k, v);
442        }
443        let query_pairs: Vec<(String, String)> = order
444            .into_iter()
445            .map(|k| {
446                let v = map.remove(&k).unwrap_or_default();
447                (k, v)
448            })
449            .collect();
450
451        url.query_pairs_mut().extend_pairs(query_pairs);
452
453        Ok(Self {
454            url: Rc::new(RefCell::new(url)),
455        })
456    }
457}
458
459/// Live iterator over a [`URLSearchParams`]. Re-reads on each `next()` so
460/// mutations during iteration are observed.
461#[derive(Trace, rquickjs::JsLifetime)]
462#[rquickjs::class]
463pub struct URLSearchParamsIter<'js> {
464    params: Class<'js, URLSearchParams>,
465    #[qjs(skip_trace)]
466    index: usize,
467    #[qjs(skip_trace)]
468    kind: IterKind,
469}
470
471impl<'js> URLSearchParamsIter<'js> {
472    fn new(
473        ctx: &Ctx<'js>,
474        params: Class<'js, URLSearchParams>,
475        kind: IterKind,
476    ) -> Result<Class<'js, Self>> {
477        live_iterator(
478            ctx,
479            Self {
480                params,
481                index: 0,
482                kind,
483            },
484        )
485    }
486}
487
488#[rquickjs::methods]
489impl<'js> URLSearchParamsIter<'js> {
490    fn next(&mut self, ctx: Ctx<'js>) -> Result<Object<'js>> {
491        let entry = self.params.borrow().read_entry(self.index, &ctx)?;
492        if entry.is_some() {
493            self.index += 1;
494        }
495        iterator_result(&ctx, self.kind, entry)
496    }
497
498    #[qjs(rename = PredefinedAtom::SymbolIterator)]
499    fn iter(this: This<Class<'js, Self>>) -> Class<'js, Self> {
500        this.0
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use crate::test::test_sync_with;
507    use rquickjs::{CatchResultExt, Class};
508
509    use super::*;
510
511    fn setup(ctx: &rquickjs::Ctx) {
512        BasePrimordials::init(ctx).unwrap();
513        Class::<URLSearchParams>::define(&ctx.globals()).unwrap();
514    }
515
516    #[tokio::test]
517    async fn test_basic() {
518        test_sync_with(|ctx| {
519            setup(&ctx);
520            let result = ctx
521                .eval::<String, _>(
522                    r#"
523                const params = new URLSearchParams();
524                params.append('a', '1');
525                params.append('b', '2');
526                params.append('a', '3');
527                params.append('b', '4');
528                params.append('c', 8);
529                params.delete('a');
530                params.delete('b', '2');
531                params.toString()
532            "#,
533                )
534                .catch(&ctx)
535                .unwrap();
536            assert_eq!(result, "b=4&c=8");
537            Ok(())
538        })
539        .await
540    }
541
542    #[tokio::test]
543    async fn test_iterate() {
544        test_sync_with(|ctx| {
545            setup(&ctx);
546            let result = ctx
547                .eval::<String, _>(
548                    r#"
549                const params = new URLSearchParams();
550                params.append('a', '1');
551                params.append('b', '2');
552                params.append('a', '3');
553                let res = [];
554                for (const [name, value] of params) {
555                    res.push(`${name}=${value}`);
556                }
557                res.join('&')
558            "#,
559                )
560                .catch(&ctx)
561                .unwrap();
562            assert_eq!(result, "a=1&b=2&a=3");
563            Ok(())
564        })
565        .await
566    }
567
568    #[tokio::test]
569    async fn test_iterate_live_delete() {
570        // Deleting the current/later entry during iteration skips it.
571        test_sync_with(|ctx| {
572            setup(&ctx);
573            let result = ctx
574                .eval::<String, _>(
575                    r#"
576                const params = new URLSearchParams("foo=0&baz=1&BAR=2");
577                const keys = [];
578                for (const [name] of params) {
579                    keys.push(name);
580                    params.delete("baz");
581                }
582                keys.join(",")
583            "#,
584                )
585                .catch(&ctx)
586                .unwrap();
587            assert_eq!(result, "foo,BAR");
588            Ok(())
589        })
590        .await
591    }
592
593    #[tokio::test]
594    async fn test_iterate_live_append() {
595        // Appending during iteration causes the new pair to be reached.
596        test_sync_with(|ctx| {
597            setup(&ctx);
598            let result = ctx
599                .eval::<String, _>(
600                    r#"
601                const params = new URLSearchParams("foo=0&baz=1");
602                const keys = [];
603                for (const [name] of params) {
604                    keys.push(name);
605                    if (name === "baz") params.append("end", "9");
606                }
607                keys.join(",")
608            "#,
609                )
610                .catch(&ctx)
611                .unwrap();
612            assert_eq!(result, "foo,baz,end");
613            Ok(())
614        })
615        .await
616    }
617
618    #[tokio::test]
619    async fn test_iterate_keys_values_live() {
620        // keys()/values() must also be live index-based iterators.
621        test_sync_with(|ctx| {
622            setup(&ctx);
623            let result = ctx
624                .eval::<String, _>(
625                    r#"
626                const params = new URLSearchParams("a=1&b=2&c=3");
627                const k = [...params.keys()].join(",");
628                const v = [...params.values()].join(",");
629                `${k}|${v}`
630            "#,
631                )
632                .catch(&ctx)
633                .unwrap();
634            assert_eq!(result, "a,b,c|1,2,3");
635            Ok(())
636        })
637        .await
638    }
639
640    #[tokio::test]
641    async fn test_iterate_entries() {
642        test_sync_with(|ctx| {
643            setup(&ctx);
644            let result = ctx
645                .eval::<String, _>(
646                    r#"
647                const params = new URLSearchParams();
648                params.append('a', '1');
649                params.append('b', '2');
650                params.append('a', '3');
651                let res = [];
652                for (const [name, value] of params.entries()) {
653                    res.push(`${name}=${value}`);
654                }
655                res.join('&')
656            "#,
657                )
658                .catch(&ctx)
659                .unwrap();
660            assert_eq!(result, "a=1&b=2&a=3");
661            Ok(())
662        })
663        .await
664    }
665
666    #[tokio::test]
667    async fn test_iterate_keys() {
668        test_sync_with(|ctx| {
669            setup(&ctx);
670            let result = ctx
671                .eval::<String, _>(
672                    r#"
673                const params = new URLSearchParams();
674                params.append('a', '1');
675                params.append('b', '2');
676                params.append('a', '3');
677                let res = [];
678                for (const name of params.keys()) {
679                    res.push(name);
680                }
681                res.join('&')
682            "#,
683                )
684                .catch(&ctx)
685                .unwrap();
686            assert_eq!(result, "a&b&a");
687            Ok(())
688        })
689        .await
690    }
691
692    #[tokio::test]
693    async fn test_iterate_values() {
694        test_sync_with(|ctx| {
695            setup(&ctx);
696            let result = ctx
697                .eval::<String, _>(
698                    r#"
699                const params = new URLSearchParams();
700                params.append('a', '1');
701                params.append('b', '2');
702                params.append('a', '3');
703                let res = [];
704                for (const name of params.values()) {
705                    res.push(name);
706                }
707                res.join('&')
708            "#,
709                )
710                .catch(&ctx)
711                .unwrap();
712            assert_eq!(result, "1&2&3");
713            Ok(())
714        })
715        .await
716    }
717
718    #[tokio::test]
719    async fn test_new_string() {
720        test_sync_with(|ctx| {
721            setup(&ctx);
722            let result = ctx
723                .eval::<String, _>(
724                    r#"
725                const params = new URLSearchParams('a=1&b=2&a=3');
726                params.toString()
727            "#,
728                )
729                .catch(&ctx)
730                .unwrap();
731            assert_eq!(result, "a=1&b=2&a=3");
732            Ok(())
733        })
734        .await
735    }
736
737    #[tokio::test]
738    async fn test_new_string_url() {
739        test_sync_with(|ctx| {
740            setup(&ctx);
741            let result = ctx
742                .eval::<String, _>(
743                    r#"
744                const params = new URLSearchParams('https://google.com?a=1&b=2&a=3');
745                params.toString()
746            "#,
747                )
748                .catch(&ctx)
749                .unwrap();
750            assert_eq!(result, "https%3A%2F%2Fgoogle.com%3Fa=1&b=2&a=3");
751            Ok(())
752        })
753        .await
754    }
755
756    #[tokio::test]
757    async fn test_new_object() {
758        test_sync_with(|ctx| {
759            setup(&ctx);
760            let result = ctx
761                .eval::<String, _>(
762                    r#"
763                const params = new URLSearchParams({'a': 1, 'b': 2});
764                params.toString()
765            "#,
766                )
767                .catch(&ctx)
768                .unwrap();
769            assert_eq!(result, "a=1&b=2");
770            Ok(())
771        })
772        .await
773    }
774
775    #[tokio::test]
776    async fn test_new_array() {
777        test_sync_with(|ctx| {
778            setup(&ctx);
779            let result = ctx
780                .eval::<String, _>(
781                    r#"
782                const params = new URLSearchParams([['a', 1], ['b', 2], ['a', 3]]);
783                params.toString()
784            "#,
785                )
786                .catch(&ctx)
787                .unwrap();
788            assert_eq!(result, "a=1&b=2&a=3");
789            Ok(())
790        })
791        .await
792    }
793
794    #[tokio::test]
795    async fn test_new_iterator() {
796        test_sync_with(|ctx| {
797            setup(&ctx);
798            let result = ctx
799                .eval::<String, _>(
800                    r#"
801                const params = new URLSearchParams();
802                params.append('a', '1');
803                params.append('b', '2');
804                params.append('a', '3');
805                const params2 = new URLSearchParams(params.entries());
806                params2.toString()
807            "#,
808                )
809                .catch(&ctx)
810                .unwrap();
811            assert_eq!(result, "a=1&b=2&a=3");
812            Ok(())
813        })
814        .await
815    }
816
817    #[tokio::test]
818    async fn test_size() {
819        test_sync_with(|ctx| {
820            setup(&ctx);
821            let result = ctx
822                .eval::<usize, _>(
823                    r#"
824                const params = new URLSearchParams();
825                params.append('a', '1');
826                params.append('b', '2');
827                params.append('a', '3');
828                params.size
829            "#,
830                )
831                .catch(&ctx)
832                .unwrap();
833            assert_eq!(result, 3);
834            Ok(())
835        })
836        .await
837    }
838
839    #[tokio::test]
840    async fn test_set() {
841        test_sync_with(|ctx| {
842            setup(&ctx);
843            let result = ctx
844                .eval::<String, _>(
845                    r#"
846                const params = new URLSearchParams();
847                params.append('a', '1');
848                params.append('b', '2');
849                params.append('a', '3');
850                params.set('a', '4');
851                params.toString()
852            "#,
853                )
854                .catch(&ctx)
855                .unwrap();
856            assert_eq!(result, "a=4&b=2");
857            Ok(())
858        })
859        .await
860    }
861
862    #[tokio::test]
863    async fn test_get() {
864        test_sync_with(|ctx| {
865            setup(&ctx);
866            let result = ctx
867                .eval::<String, _>(
868                    r#"
869                const params = new URLSearchParams();
870                params.append('a', '1');
871                params.append('b', '2');
872                params.append('a', '3');
873                params.get('a')
874            "#,
875                )
876                .catch(&ctx)
877                .unwrap();
878            assert_eq!(result, "1");
879            Ok(())
880        })
881        .await
882    }
883
884    #[tokio::test]
885    async fn test_get_missing() {
886        test_sync_with(|ctx| {
887            setup(&ctx);
888            let result = ctx
889                .eval::<bool, _>(
890                    r#"
891                const params = new URLSearchParams();
892                params.append('a', '1');
893                params.append('b', '2');
894                params.append('a', '3');
895                params.get('c') === null
896            "#,
897                )
898                .catch(&ctx)
899                .unwrap();
900            assert!(result);
901            Ok(())
902        })
903        .await
904    }
905
906    #[tokio::test]
907    async fn test_get_all() {
908        test_sync_with(|ctx| {
909            setup(&ctx);
910            let result = ctx
911                .eval::<String, _>(
912                    r#"
913                const params = new URLSearchParams();
914                params.append('a', '1');
915                params.append('b', '2');
916                params.append('a', '3');
917                params.getAll('a').join('&')
918            "#,
919                )
920                .catch(&ctx)
921                .unwrap();
922            assert_eq!(result, "1&3");
923            Ok(())
924        })
925        .await
926    }
927
928    #[tokio::test]
929    async fn test_get_all_missing() {
930        test_sync_with(|ctx| {
931            setup(&ctx);
932            let result = ctx
933                .eval::<String, _>(
934                    r#"
935                const params = new URLSearchParams();
936                params.append('a', '1');
937                params.append('b', '2');
938                params.append('a', '3');
939                params.getAll('c').join('&')
940            "#,
941                )
942                .catch(&ctx)
943                .unwrap();
944            assert_eq!(result, "");
945            Ok(())
946        })
947        .await
948    }
949
950    #[tokio::test]
951    async fn test_has() {
952        test_sync_with(|ctx| {
953            setup(&ctx);
954            let result = ctx
955                .eval::<bool, _>(
956                    r#"
957                const params = new URLSearchParams();
958                params.append('a', '1');
959                params.append('b', '2');
960                params.append('a', '3');
961                params.has('b')
962            "#,
963                )
964                .catch(&ctx)
965                .unwrap();
966            assert!(result);
967            Ok(())
968        })
969        .await
970    }
971
972    #[tokio::test]
973    async fn test_has_value() {
974        test_sync_with(|ctx| {
975            setup(&ctx);
976            let result = ctx
977                .eval::<bool, _>(
978                    r#"
979                const params = new URLSearchParams();
980                params.append('a', '1');
981                params.append('b', '2');
982                params.append('a', '3');
983                params.has('b', 5)
984            "#,
985                )
986                .catch(&ctx)
987                .unwrap();
988            assert!(!result);
989            Ok(())
990        })
991        .await
992    }
993
994    #[tokio::test]
995    async fn test_has_not() {
996        test_sync_with(|ctx| {
997            setup(&ctx);
998            let result = ctx
999                .eval::<bool, _>(
1000                    r#"
1001                const params = new URLSearchParams();
1002                params.append('a', '1');
1003                params.append('b', '2');
1004                params.append('a', '3');
1005                params.has('c')
1006            "#,
1007                )
1008                .catch(&ctx)
1009                .unwrap();
1010            assert!(!result);
1011            Ok(())
1012        })
1013        .await
1014    }
1015
1016    #[tokio::test]
1017    async fn test_sort() {
1018        test_sync_with(|ctx| {
1019            setup(&ctx);
1020            let result = ctx
1021                .eval::<String, _>(
1022                    r#"
1023                const params = new URLSearchParams();
1024                params.append('a', '3');
1025                params.append('b', '2');
1026                params.append('a', '1');
1027                params.sort();
1028                params.toString()
1029            "#,
1030                )
1031                .catch(&ctx)
1032                .unwrap();
1033            assert_eq!(result, "a=3&a=1&b=2");
1034            Ok(())
1035        })
1036        .await
1037    }
1038
1039    #[tokio::test]
1040    async fn test_for_each() {
1041        test_sync_with(|ctx| {
1042            setup(&ctx);
1043            let result = ctx
1044                .eval::<String, _>(
1045                    r#"
1046                const params = new URLSearchParams();
1047                params.append('a', '3');
1048                params.append('b', '2');
1049                params.append('a', '1');
1050                let res = [];
1051                params.forEach((value, name) => {
1052                    res.push(`${name}=${value}`);
1053                });
1054                res.join('&')
1055            "#,
1056                )
1057                .catch(&ctx)
1058                .unwrap();
1059            assert_eq!(result, "a=3&b=2&a=1");
1060            Ok(())
1061        })
1062        .await
1063    }
1064}