This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/virtual_dom/
vtag.rs

1//! This module contains the implementation of a virtual element node [VTag].
2
3use std::cmp::PartialEq;
4use std::marker::PhantomData;
5use std::mem;
6use std::ops::{Deref, DerefMut};
7use std::rc::Rc;
8
9use wasm_bindgen::JsValue;
10use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
11
12use super::{AttrValue, AttributeOrProperty, Attributes, Key, Listener, Listeners, VNode};
13use crate::html::{ImplicitClone, IntoPropValue, NodeRef};
14
15/// SVG namespace string used for creating svg elements
16pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
17
18/// MathML namespace string used for creating MathML elements
19pub const MATHML_NAMESPACE: &str = "http://www.w3.org/1998/Math/MathML";
20
21/// Default namespace for html elements
22pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
23
24/// Value field corresponding to an [Element]'s `value` property
25#[derive(Debug, Eq, PartialEq)]
26pub(crate) struct Value<T>(Option<AttrValue>, PhantomData<T>);
27
28impl<T> Clone for Value<T> {
29    fn clone(&self) -> Self {
30        Self::new(self.0.clone())
31    }
32}
33
34impl<T> ImplicitClone for Value<T> {}
35
36impl<T> Default for Value<T> {
37    fn default() -> Self {
38        Self::new(None)
39    }
40}
41
42impl<T> Value<T> {
43    /// Create a new value. The caller should take care that the value is valid for the element's
44    /// `value` property
45    fn new(value: Option<AttrValue>) -> Self {
46        Value(value, PhantomData)
47    }
48
49    /// Set a new value. The caller should take care that the value is valid for the element's
50    /// `value` property
51    pub(crate) fn set(&mut self, value: Option<AttrValue>) {
52        self.0 = value;
53    }
54}
55
56impl<T> Deref for Value<T> {
57    type Target = Option<AttrValue>;
58
59    fn deref(&self) -> &Self::Target {
60        &self.0
61    }
62}
63
64/// Fields specific to
65/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag](crate::virtual_dom::VTag)s
66#[derive(Debug, Clone, Default, Eq, PartialEq)]
67pub(crate) struct InputFields {
68    /// Contains a value of an
69    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
70    pub(crate) value: Value<InputElement>,
71    /// Represents `checked` attribute of
72    /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked).
73    /// It exists to override standard behavior of `checked` attribute, because
74    /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive
75    /// frameworks it's more useful to control `checked` value of an `InputElement`.
76    pub(crate) checked: Option<bool>,
77}
78
79impl ImplicitClone for InputFields {}
80
81impl Deref for InputFields {
82    type Target = Value<InputElement>;
83
84    fn deref(&self) -> &Self::Target {
85        &self.value
86    }
87}
88
89impl DerefMut for InputFields {
90    fn deref_mut(&mut self) -> &mut Self::Target {
91        &mut self.value
92    }
93}
94
95impl InputFields {
96    /// Create new attributes for an [InputElement] element
97    fn new(value: Option<AttrValue>, checked: Option<bool>) -> Self {
98        Self {
99            value: Value::new(value),
100            checked,
101        }
102    }
103}
104
105#[derive(Debug, Clone, Default)]
106pub(crate) struct TextareaFields {
107    /// Contains the value of an
108    /// [TextAreaElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).
109    pub(crate) value: Value<TextAreaElement>,
110    /// Contains the default value of
111    /// [TextAreaElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).
112    #[allow(unused)] // unused only if both "csr" and "ssr" features are off
113    pub(crate) defaultvalue: Option<AttrValue>,
114}
115
116/// [VTag] fields that are specific to different [VTag] kinds.
117/// Decreases the memory footprint of [VTag] by avoiding impossible field and value combinations.
118#[derive(Debug, Clone)]
119pub(crate) enum VTagInner {
120    /// Fields specific to
121    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)
122    /// [VTag]s
123    Input(InputFields),
124    /// Fields specific to
125    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
126    /// [VTag]s
127    Textarea(TextareaFields),
128    /// Fields for all other kinds of [VTag]s
129    Other {
130        /// A tag of the element.
131        tag: AttrValue,
132        /// children of the element.
133        children: VNode,
134    },
135}
136
137impl ImplicitClone for VTagInner {}
138
139/// A type for a virtual
140/// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)
141/// representation.
142#[derive(Debug, Clone)]
143pub struct VTag {
144    /// [VTag] fields that are specific to different [VTag] kinds.
145    pub(crate) inner: VTagInner,
146    /// List of attached listeners.
147    pub(crate) listeners: Listeners,
148    /// A node reference used for DOM access in Component lifecycle methods
149    pub node_ref: NodeRef,
150    /// List of attributes.
151    pub attributes: Attributes,
152    pub key: Option<Key>,
153}
154
155impl ImplicitClone for VTag {}
156
157impl VTag {
158    /// Creates a new [VTag] instance with `tag` name (cannot be changed later in DOM).
159    pub fn new(tag: impl Into<AttrValue>) -> Self {
160        let tag = tag.into();
161        Self::new_base(
162            match &*tag.to_ascii_lowercase() {
163                "input" => VTagInner::Input(Default::default()),
164                "textarea" => VTagInner::Textarea(Default::default()),
165                _ => VTagInner::Other {
166                    tag,
167                    children: Default::default(),
168                },
169            },
170            Default::default(),
171            Default::default(),
172            Default::default(),
173            Default::default(),
174        )
175    }
176
177    /// Creates a new
178    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag]
179    /// instance.
180    ///
181    /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the
182    /// compiler to inline property and child list construction in the `html!` macro. This enables
183    /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag
184    /// fields.
185    #[doc(hidden)]
186    #[allow(clippy::too_many_arguments)]
187    pub fn __new_input(
188        value: Option<AttrValue>,
189        checked: Option<bool>,
190        node_ref: NodeRef,
191        key: Option<Key>,
192        // at the bottom for more readable macro-expanded code
193        attributes: Attributes,
194        listeners: Listeners,
195    ) -> Self {
196        VTag::new_base(
197            VTagInner::Input(InputFields::new(
198                value,
199                // In HTML node `checked` attribute sets `defaultChecked` parameter,
200                // but we use own field to control real `checked` parameter
201                checked,
202            )),
203            node_ref,
204            key,
205            attributes,
206            listeners,
207        )
208    }
209
210    /// Creates a new
211    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) [VTag]
212    /// instance.
213    ///
214    /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the
215    /// compiler to inline property and child list construction in the `html!` macro. This enables
216    /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag
217    /// fields.
218    #[doc(hidden)]
219    #[allow(clippy::too_many_arguments)]
220    pub fn __new_textarea(
221        value: Option<AttrValue>,
222        defaultvalue: Option<AttrValue>,
223        node_ref: NodeRef,
224        key: Option<Key>,
225        // at the bottom for more readable macro-expanded code
226        attributes: Attributes,
227        listeners: Listeners,
228    ) -> Self {
229        VTag::new_base(
230            VTagInner::Textarea(TextareaFields {
231                value: Value::new(value),
232                defaultvalue,
233            }),
234            node_ref,
235            key,
236            attributes,
237            listeners,
238        )
239    }
240
241    /// Creates a new [VTag] instance with `tag` name (cannot be changed later in DOM).
242    ///
243    /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the
244    /// compiler to inline property and child list construction in the `html!` macro. This enables
245    /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag
246    /// fields.
247    #[doc(hidden)]
248    #[allow(clippy::too_many_arguments)]
249    pub fn __new_other(
250        tag: AttrValue,
251        node_ref: NodeRef,
252        key: Option<Key>,
253        // at the bottom for more readable macro-expanded code
254        attributes: Attributes,
255        listeners: Listeners,
256        children: VNode,
257    ) -> Self {
258        VTag::new_base(
259            VTagInner::Other { tag, children },
260            node_ref,
261            key,
262            attributes,
263            listeners,
264        )
265    }
266
267    /// Constructs a [VTag] from [VTagInner] and fields common to all [VTag] kinds
268    #[inline]
269    #[allow(clippy::too_many_arguments)]
270    fn new_base(
271        inner: VTagInner,
272        node_ref: NodeRef,
273        key: Option<Key>,
274        attributes: Attributes,
275        listeners: Listeners,
276    ) -> Self {
277        VTag {
278            inner,
279            attributes,
280            listeners,
281            node_ref,
282            key,
283        }
284    }
285
286    /// Returns tag of an [Element](web_sys::Element). In HTML tags are always uppercase.
287    pub fn tag(&self) -> &str {
288        match &self.inner {
289            VTagInner::Input { .. } => "input",
290            VTagInner::Textarea { .. } => "textarea",
291            VTagInner::Other { tag, .. } => tag.as_ref(),
292        }
293    }
294
295    /// Add [VNode] child.
296    pub fn add_child(&mut self, child: VNode) {
297        if let VTagInner::Other { children, .. } = &mut self.inner {
298            children.to_vlist_mut().add_child(child)
299        }
300    }
301
302    /// Add multiple [VNode] children.
303    pub fn add_children(&mut self, children: impl IntoIterator<Item = VNode>) {
304        if let VTagInner::Other { children: dst, .. } = &mut self.inner {
305            dst.to_vlist_mut().add_children(children)
306        }
307    }
308
309    /// Returns a reference to the children of this [VTag], if the node can have
310    /// children
311    pub fn children(&self) -> Option<&VNode> {
312        match &self.inner {
313            VTagInner::Other { children, .. } => Some(children),
314            _ => None,
315        }
316    }
317
318    /// Returns a mutable reference to the children of this [VTag], if the node can have
319    /// children
320    pub fn children_mut(&mut self) -> Option<&mut VNode> {
321        match &mut self.inner {
322            VTagInner::Other { children, .. } => Some(children),
323            _ => None,
324        }
325    }
326
327    /// Returns the children of this [VTag], if the node can have
328    /// children
329    pub fn into_children(self) -> Option<VNode> {
330        match self.inner {
331            VTagInner::Other { children, .. } => Some(children),
332            _ => None,
333        }
334    }
335
336    /// Returns the `value` of an
337    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) or
338    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
339    pub fn value(&self) -> Option<&AttrValue> {
340        match &self.inner {
341            VTagInner::Input(f) => f.as_ref(),
342            VTagInner::Textarea(TextareaFields { value, .. }) => value.as_ref(),
343            VTagInner::Other { .. } => None,
344        }
345    }
346
347    /// Sets `value` for an
348    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) or
349    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
350    pub fn set_value(&mut self, value: impl IntoPropValue<Option<AttrValue>>) {
351        match &mut self.inner {
352            VTagInner::Input(f) => {
353                f.set(value.into_prop_value());
354            }
355            VTagInner::Textarea(TextareaFields { value: dst, .. }) => {
356                dst.set(value.into_prop_value());
357            }
358            VTagInner::Other { .. } => (),
359        }
360    }
361
362    /// Returns `checked` property of an
363    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
364    /// (Does not affect the value of the node's attribute).
365    pub fn checked(&self) -> Option<bool> {
366        match &self.inner {
367            VTagInner::Input(f) => f.checked,
368            _ => None,
369        }
370    }
371
372    /// Sets `checked` property of an
373    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
374    /// (Does not affect the value of the node's attribute).
375    pub fn set_checked(&mut self, value: bool) {
376        if let VTagInner::Input(f) = &mut self.inner {
377            f.checked = Some(value);
378        }
379    }
380
381    /// Keeps the current value of the `checked` property of an
382    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
383    /// (Does not affect the value of the node's attribute).
384    pub fn preserve_checked(&mut self) {
385        if let VTagInner::Input(f) = &mut self.inner {
386            f.checked = None;
387        }
388    }
389
390    /// Adds a key-value pair to attributes
391    ///
392    /// Not every attribute works when it set as an attribute. We use workarounds for:
393    /// `value` and `checked`.
394    pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
395        self.attributes.get_mut_index_map().insert(
396            AttrValue::Static(key),
397            AttributeOrProperty::Attribute(value.into()),
398        );
399    }
400
401    /// Set the given key as property on the element
402    ///
403    /// [`js_sys::Reflect`] is used for setting properties.
404    pub fn add_property(&mut self, key: &'static str, value: impl Into<JsValue>) {
405        self.attributes.get_mut_index_map().insert(
406            AttrValue::Static(key),
407            AttributeOrProperty::Property(value.into()),
408        );
409    }
410
411    /// Sets attributes to a virtual node.
412    ///
413    /// Not every attribute works when it set as an attribute. We use workarounds for:
414    /// `value` and `checked`.
415    pub fn set_attributes(&mut self, attrs: impl Into<Attributes>) {
416        self.attributes = attrs.into();
417    }
418
419    #[doc(hidden)]
420    pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue<AttrValue>) {
421        self.attributes.get_mut_index_map().insert(
422            AttrValue::from(key),
423            AttributeOrProperty::Attribute(value.into_prop_value()),
424        );
425    }
426
427    /// Add event listener on the [VTag]'s  [Element](web_sys::Element).
428    /// Returns `true` if the listener has been added, `false` otherwise.
429    pub fn add_listener(&mut self, listener: Rc<dyn Listener>) -> bool {
430        match &mut self.listeners {
431            Listeners::None => {
432                self.set_listeners([Some(listener)].into());
433                true
434            }
435            Listeners::Pending(listeners) => {
436                let mut listeners = mem::take(listeners).into_vec();
437                listeners.push(Some(listener));
438
439                self.set_listeners(listeners.into());
440                true
441            }
442        }
443    }
444
445    /// Set event listeners on the [VTag]'s  [Element](web_sys::Element)
446    pub fn set_listeners(&mut self, listeners: Box<[Option<Rc<dyn Listener>>]>) {
447        self.listeners = Listeners::Pending(listeners);
448    }
449}
450
451impl PartialEq for VTag {
452    fn eq(&self, other: &VTag) -> bool {
453        use VTagInner::*;
454
455        (match (&self.inner, &other.inner) {
456            (Input(l), Input(r)) => l == r,
457            (Textarea (TextareaFields{ value: value_l, .. }), Textarea (TextareaFields{ value: value_r, .. })) => value_l == value_r,
458            (Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r,
459            _ => false,
460        }) && self.listeners.eq(&other.listeners)
461            && self.attributes == other.attributes
462            // Diff children last, as recursion is the most expensive
463            && match (&self.inner, &other.inner) {
464                (Other { children: ch_l, .. }, Other { children: ch_r, .. }) => ch_l == ch_r,
465                _ => true,
466            }
467    }
468}
469
470#[cfg(feature = "ssr")]
471mod feat_ssr {
472    use std::fmt::Write;
473
474    use super::*;
475    use crate::feat_ssr::VTagKind;
476    use crate::html::AnyScope;
477    use crate::platform::fmt::BufWriter;
478    use crate::virtual_dom::VText;
479
480    // Elements that cannot have any child elements.
481    static VOID_ELEMENTS: &[&str; 15] = &[
482        "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
483        "source", "track", "wbr", "textarea",
484    ];
485
486    impl VTag {
487        pub(crate) async fn render_into_stream(
488            &self,
489            w: &mut BufWriter,
490            parent_scope: &AnyScope,
491            hydratable: bool,
492        ) {
493            let _ = w.write_str("<");
494            let _ = w.write_str(self.tag());
495
496            let write_attr = |w: &mut BufWriter, name: &str, val: Option<&str>| {
497                let _ = w.write_str(" ");
498                let _ = w.write_str(name);
499
500                if let Some(m) = val {
501                    let _ = w.write_str("=\"");
502                    let _ = w.write_str(&html_escape::encode_double_quoted_attribute(m));
503                    let _ = w.write_str("\"");
504                }
505            };
506
507            if let VTagInner::Input(InputFields { value, checked }) = &self.inner {
508                if let Some(value) = value.as_deref() {
509                    write_attr(w, "value", Some(value));
510                }
511
512                // Setting is as an attribute sets the `defaultChecked` property. Only emit this
513                // if it's explicitly set to checked.
514                if *checked == Some(true) {
515                    write_attr(w, "checked", None);
516                }
517            }
518
519            for (k, v) in self.attributes.iter() {
520                write_attr(w, k, Some(v));
521            }
522
523            let _ = w.write_str(">");
524
525            match &self.inner {
526                VTagInner::Input(_) => {}
527                VTagInner::Textarea(TextareaFields {
528                    value,
529                    defaultvalue,
530                }) => {
531                    if let Some(def) = value.as_ref().or(defaultvalue.as_ref()) {
532                        VText::new(def.clone())
533                            .render_into_stream(w, parent_scope, hydratable, VTagKind::Other)
534                            .await;
535                    }
536
537                    let _ = w.write_str("</textarea>");
538                }
539                VTagInner::Other { tag, children } => {
540                    if !VOID_ELEMENTS.contains(&tag.as_ref()) {
541                        children
542                            .render_into_stream(w, parent_scope, hydratable, tag.into())
543                            .await;
544
545                        let _ = w.write_str("</");
546                        let _ = w.write_str(tag);
547                        let _ = w.write_str(">");
548                    } else {
549                        // We don't write children of void elements nor closing tags.
550                        debug_assert!(
551                            match children {
552                                VNode::VList(m) => m.is_empty(),
553                                _ => false,
554                            },
555                            "{tag} cannot have any children!"
556                        );
557                    }
558                }
559            }
560        }
561    }
562}
563
564#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
565#[cfg(feature = "ssr")]
566#[cfg(test)]
567mod ssr_tests {
568    use tokio::test;
569
570    use crate::prelude::*;
571    use crate::LocalServerRenderer as ServerRenderer;
572
573    #[cfg_attr(not(target_os = "wasi"), test)]
574    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
575    async fn test_simple_tag() {
576        #[function_component]
577        fn Comp() -> Html {
578            html! { <div></div> }
579        }
580
581        let s = ServerRenderer::<Comp>::new()
582            .hydratable(false)
583            .render()
584            .await;
585
586        assert_eq!(s, "<div></div>");
587    }
588
589    #[cfg_attr(not(target_os = "wasi"), test)]
590    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
591    async fn test_simple_tag_with_attr() {
592        #[function_component]
593        fn Comp() -> Html {
594            html! { <div class="abc"></div> }
595        }
596
597        let s = ServerRenderer::<Comp>::new()
598            .hydratable(false)
599            .render()
600            .await;
601
602        assert_eq!(s, r#"<div class="abc"></div>"#);
603    }
604
605    #[cfg_attr(not(target_os = "wasi"), test)]
606    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
607    async fn test_simple_tag_with_content() {
608        #[function_component]
609        fn Comp() -> Html {
610            html! { <div>{"Hello!"}</div> }
611        }
612
613        let s = ServerRenderer::<Comp>::new()
614            .hydratable(false)
615            .render()
616            .await;
617
618        assert_eq!(s, r#"<div>Hello!</div>"#);
619    }
620
621    #[cfg_attr(not(target_os = "wasi"), test)]
622    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
623    async fn test_simple_tag_with_nested_tag_and_input() {
624        #[function_component]
625        fn Comp() -> Html {
626            html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
627        }
628
629        let s = ServerRenderer::<Comp>::new()
630            .hydratable(false)
631            .render()
632            .await;
633
634        assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
635    }
636
637    #[cfg_attr(not(target_os = "wasi"), test)]
638    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
639    async fn test_textarea() {
640        #[function_component]
641        fn Comp() -> Html {
642            html! { <textarea value="teststring" /> }
643        }
644
645        let s = ServerRenderer::<Comp>::new()
646            .hydratable(false)
647            .render()
648            .await;
649
650        assert_eq!(s, r#"<textarea>teststring</textarea>"#);
651    }
652
653    #[cfg_attr(not(target_os = "wasi"), test)]
654    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
655    async fn test_textarea_w_defaultvalue() {
656        #[function_component]
657        fn Comp() -> Html {
658            html! { <textarea defaultvalue="teststring" /> }
659        }
660
661        let s = ServerRenderer::<Comp>::new()
662            .hydratable(false)
663            .render()
664            .await;
665
666        assert_eq!(s, r#"<textarea>teststring</textarea>"#);
667    }
668
669    #[cfg_attr(not(target_os = "wasi"), test)]
670    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
671    async fn test_value_precedence_over_defaultvalue() {
672        #[function_component]
673        fn Comp() -> Html {
674            html! { <textarea defaultvalue="defaultvalue" value="value" /> }
675        }
676
677        let s = ServerRenderer::<Comp>::new()
678            .hydratable(false)
679            .render()
680            .await;
681
682        assert_eq!(s, r#"<textarea>value</textarea>"#);
683    }
684
685    #[cfg_attr(not(target_os = "wasi"), test)]
686    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
687    async fn test_escaping_in_style_tag() {
688        #[function_component]
689        fn Comp() -> Html {
690            html! { <style>{"body > a {color: #cc0;}"}</style> }
691        }
692
693        let s = ServerRenderer::<Comp>::new()
694            .hydratable(false)
695            .render()
696            .await;
697
698        assert_eq!(s, r#"<style>body > a {color: #cc0;}</style>"#);
699    }
700
701    #[cfg_attr(not(target_os = "wasi"), test)]
702    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
703    async fn test_escaping_in_script_tag() {
704        #[function_component]
705        fn Comp() -> Html {
706            html! { <script>{"foo.bar = x < y;"}</script> }
707        }
708
709        let s = ServerRenderer::<Comp>::new()
710            .hydratable(false)
711            .render()
712            .await;
713
714        assert_eq!(s, r#"<script>foo.bar = x < y;</script>"#);
715    }
716
717    #[cfg_attr(not(target_os = "wasi"), test)]
718    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
719    async fn test_multiple_vtext_in_style_tag() {
720        #[function_component]
721        fn Comp() -> Html {
722            let one = "html { background: black } ";
723            let two = "body > a { color: white } ";
724            html! {
725                <style>
726                    {one}
727                    {two}
728                </style>
729            }
730        }
731
732        let s = ServerRenderer::<Comp>::new()
733            .hydratable(false)
734            .render()
735            .await;
736
737        assert_eq!(
738            s,
739            r#"<style>html { background: black } body > a { color: white } </style>"#
740        );
741    }
742}