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