1use 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#[derive(Clone, Trace, rquickjs::JsLifetime)]
35#[rquickjs::class]
36pub struct URLSearchParams {
37 #[qjs(skip_trace)]
41 pub url: Rc<RefCell<Url>>,
42}
43
44#[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 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 #[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 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 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 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 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 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 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 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 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 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#[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 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 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 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}