1context("tags")
2
3test_that("Basic tag writing works", {
4  expect_equal(as.character(tagList("hi")), "hi")
5  expect_equal(
6    as.character(tagList("one", "two", tagList("three"))),
7    "one\ntwo\nthree")
8  expect_equal(
9    as.character(tags$b("one")),
10    "<b>one</b>")
11  expect_equal(
12    as.character(tags$b("one", "two")),
13    "<b>\n  one\n  two\n</b>")
14  expect_equal(
15    as.character(tagList(list("one"))),
16    "one")
17  expect_equal(
18    as.character(tagList(list(tagList("one")))),
19    "one")
20  expect_equal(
21    as.character(tagList(tags$br(), "one")),
22    "<br/>\none")
23})
24
25test_that("Hanging commas don't break things", {
26  expect_equal(as.character(tagList("hi",)), "hi")
27  expect_equal(as.character(div("one",)), "<div>one</div>")
28  # Multiple commas still throw
29  expect_error(as.character(div("one",,)), "is empty")
30  # Non-trailing commas still throw
31  expect_error(as.character(div(,"one",)), "is empty")
32})
33
34
35test_that("withTags works", {
36  output_tags <- tags$div(class = "myclass",
37    tags$h3("header"),
38    tags$p("text here")
39  )
40  output_withhtml <- withTags(
41    div(class = "myclass",
42      h3("header"),
43      p("text here")
44    )
45  )
46  expect_identical(output_tags, output_withhtml)
47
48
49  # Check that current environment is searched
50  x <- 100
51  expect_identical(tags$p(x), withTags(p(x)))
52
53  # Just to make sure, run it in a function, which has its own environment
54  foo <- function() {
55    y <- 100
56    withTags(p(y))
57  }
58  expect_identical(tags$p(100), foo())
59})
60
61test_that(".noWS argument of withTags()", {
62  get_noWS <- function(tag) tag[[".noWS"]]
63
64  default <- withTags(
65    div(
66      class = "myclass",
67      h3("header"),
68      p("One", strong(span("two")), "three")
69    )
70  )
71
72  expect_null(get_noWS(default))
73  expect_null(get_noWS(default$children[[1]]))
74  expect_null(get_noWS(default$children[[2]]))
75  expect_null(get_noWS(default$children[[2]]$children[[2]]))
76  expect_null(get_noWS(default$children[[2]]$children[[2]]$children[[1]]))
77
78  default_special <- withTags(
79    div(
80      class = "myclass",
81      h3("header", .noWS = "after-begin"),
82      p("One", strong(span("two")), "three", .noWS = "before-end")
83    )
84  )
85
86  expect_null(get_noWS(default_special))
87  expect_equal(get_noWS(default_special$children[[1]]), "after-begin")
88  expect_equal(get_noWS(default_special$children[[2]]), "before-end")
89  expect_null(get_noWS(default_special$children[[2]]$children[[2]]))
90  expect_null(get_noWS(default_special$children[[2]]$children[[2]]$children[[1]]))
91
92  all_same_noWS <- c("outside", "inside")
93  all_same <- withTags(
94    div(
95      class = "myclass",
96      h3("header"),
97      p("One", strong(span("two")), "three")
98    ),
99    .noWS = all_same_noWS
100  )
101
102  expect_equal(get_noWS(all_same), all_same_noWS)
103  expect_equal(get_noWS(all_same$children[[1]]), all_same_noWS)
104  expect_equal(get_noWS(all_same$children[[2]]), all_same_noWS)
105  expect_equal(get_noWS(all_same$children[[2]]$children[[2]]), all_same_noWS)
106  expect_equal(get_noWS(all_same$children[[2]]$children[[2]]$children[[1]]), all_same_noWS)
107
108  varied_default <- "outside"
109  varied_special <- "inside"
110  varied <- withTags(
111    div(
112      class = "myclass",
113      h3("header"),
114      p("One", strong(span("two"), .noWS = varied_special), "three")
115    ),
116    .noWS = varied_default
117  )
118
119  expect_equal(get_noWS(varied), varied_default)
120  expect_equal(get_noWS(varied$children[[1]]), varied_default)
121  expect_equal(get_noWS(varied$children[[2]]), varied_default)
122  expect_equal(get_noWS(varied$children[[2]]$children[[2]]), varied_special)
123  expect_equal(get_noWS(varied$children[[2]]$children[[2]]$children[[1]]), varied_default)
124})
125
126test_that("HTML escaping in tags", {
127  # Regular text is escaped
128  expect_equivalent(format(div("<a&b>")), "<div>&lt;a&amp;b&gt;</div>")
129
130  # Text in HTML() isn't escaped
131  expect_equivalent(format(div(HTML("<a&b>"))), "<div><a&b></div>")
132
133  # Text in a property is escaped
134  expect_equivalent(format(div(class = "<a&b>", "text")),
135    '<div class="&lt;a&amp;b&gt;">text</div>')
136
137  # HTML() has no effect in a property like 'class'
138  expect_equivalent(format(div(class = HTML("<a&b>"), "text")),
139    '<div class="&lt;a&amp;b&gt;">text</div>')
140})
141
142
143test_that("Adding child tags", {
144  tag_list <- list(tags$p("tag1"), tags$b("tag2"), tags$i("tag3"))
145
146  # Creating nested tags by calling the tag$div function and passing a list
147  t1 <- tags$div(class="foo", tag_list)
148  expect_equal(length(t1$children), 1)
149  expect_equal(length(t1$children[[1]]), 3)
150  expect_equal(t1$children[[1]][[1]]$name, "p")
151  expect_equal(t1$children[[1]][[1]]$children[[1]], "tag1")
152  expect_equal(t1$children[[1]][[2]]$name, "b")
153  expect_equal(t1$children[[1]][[2]]$children[[1]], "tag2")
154  expect_equal(t1$children[[1]][[3]]$name, "i")
155  expect_equal(t1$children[[1]][[3]]$children[[1]], "tag3")
156
157
158  # div tag used as starting point for tests below
159  div_tag <- tags$div(class="foo")
160
161  # Appending each child
162  t2 <- tagAppendChild(div_tag, tag_list[[1]])
163  t2 <- tagAppendChild(t2, tag_list[[2]])
164  t2 <- tagAppendChild(t2, tag_list[[3]])
165  t2a <- do.call(tags$div, c(tag_list, class="foo"))
166  expect_identical(t2a, t2)
167  t2b <- tagAppendChildren(div_tag, `names_are_ignored` = tag_list[[1]],
168    "ignore-this-name" = tag_list[[2]],
169    dummyName = tag_list[[3]])
170  expect_identical(t2b, t2)
171
172  # tagSetChildren, using list argument
173  t2 <- tagSetChildren(div_tag, list = tag_list)
174  expect_identical(t2a, t2)
175
176  # tagSetChildren, using ... arguments
177  t2 <- tagSetChildren(div_tag, tag_list[[1]], tag_list[[2]], tag_list[[3]])
178  expect_identical(t2a, t2)
179
180  # tagSetChildren, using named ... arguments (names should be ignored)
181  t2 <- tagSetChildren(div_tag, ignored = tag_list[[1]], dummy = tag_list[[2]], blah = tag_list[[3]])
182  expect_identical(t2a, t2)
183
184  # tagSetChildren, using ... and list arguments
185  t2 <- tagSetChildren(div_tag, tag_list[[1]], list = tag_list[2:3])
186  expect_identical(t2a, t2)
187
188  # tagSetChildren overwrites existing children
189  t2 <- tagAppendChild(div_tag, p("should replace this tag"))
190  t2 <- tagSetChildren(div_tag, list = tag_list)
191  expect_identical(t2a, t2)
192
193
194  # tagAppendChildren, using list argument
195  t2 <- tagAppendChild(div_tag, tag_list[[1]])
196  t2 <- tagAppendChildren(t2, list = tag_list[2:3])
197  expect_identical(t2a, t2)
198
199  # tagAppendChildren, using ... arguments
200  t2 <- tagAppendChild(div_tag, tag_list[[1]])
201  t2 <- tagAppendChildren(t2, tag_list[[2]], tag_list[[3]])
202  expect_identical(t2a, t2)
203
204  # tagAppendChildren, using ... and list arguments
205  t2 <- tagAppendChild(div_tag, tag_list[[1]])
206  t2 <- tagAppendChildren(t2, tag_list[[2]], list = list(tag_list[[3]]))
207  expect_identical(t2a, t2)
208
209  # tagAppendChildren can start with no children
210  t2 <- tagAppendChildren(div_tag, list = tag_list)
211  expect_identical(t2a, t2)
212
213
214  # tagSetChildren preserves attributes
215  x <- tagSetChildren(div(), HTML("text"))
216  expect_identical(attr(x$children[[1]], "html", TRUE), TRUE)
217
218  # tagAppendChildren preserves attributes
219  x <- tagAppendChildren(div(), HTML("text"))
220  expect_identical(attr(x$children[[1]], "html", TRUE), TRUE)
221})
222
223
224test_that("Creating simple tags", {
225  # Empty tag
226  expect_identical(
227    div(),
228    structure(
229      list(name = "div", attribs = dots_list(), children = list()),
230      .Names = c("name", "attribs", "children"),
231      class = "shiny.tag"
232    )
233  )
234
235  # Tag with text
236  expect_identical(
237    div("text"),
238    structure(
239      list(name = "div", attribs = dots_list(), children = list("text")),
240      .Names = c("name", "attribs", "children"),
241      class = "shiny.tag"
242    )
243  )
244
245  # NULL attributes are dropped
246  expect_identical(
247    div(a = NULL, b = "value"),
248    div(b = "value")
249  )
250
251  # length-0 attributes are dropped
252  expect_identical(
253    div(a = character(), b = "value"),
254    div(b = "value")
255  )
256
257  # NULL children are dropped
258  expect_identical(
259    renderTags(div("foo", NULL, list(NULL, list(NULL, "bar"))))$html,
260    renderTags(div("foo", "bar"))$html
261  )
262
263  # length-0 children are dropped
264  expect_identical(
265    renderTags(div("foo", character(), list(character(), list(list(), "bar"))))$html,
266    renderTags(div("foo", "bar"))$html
267  )
268
269  # Numbers are coerced to strings
270  expect_identical(
271    renderTags(div(1234))$html,
272    renderTags(div("1234"))$html
273  )
274})
275
276
277test_that("Creating nested tags", {
278  # Simple version
279  # Note that the $children list should not have a names attribute
280  expect_identical(
281    div(class="foo", list("a", "b")),
282    structure(
283      list(name = "div",
284        attribs = structure(list(class = "foo"), .Names = "class"),
285        children = list(list("a", "b"))),
286      .Names = c("name", "attribs", "children"),
287      class = "shiny.tag"
288    )
289  )
290
291  # More complex version
292  t1 <- withTags(
293    div(class = "foo",
294      p("child tag"),
295      list(
296        p("in-list child tag 1"),
297        "in-list character string",
298        p(),
299        p("in-list child tag 2")
300      ),
301      "character string",
302      1234
303    )
304  )
305
306  # t1 should be identical to this data structure.
307  # The nested list should be flattened, and non-tag, non-strings should be
308  # converted to strings
309  t1_full <- structure(
310    list(
311      name = "div",
312      attribs = list(class = "foo"),
313      children = list(
314        structure(list(name = "p",
315          attribs = list(),
316          children = list("child tag")),
317          class = "shiny.tag"
318        ),
319        structure(list(name = "p",
320          attribs = list(),
321          children = list("in-list child tag 1")),
322          class = "shiny.tag"
323        ),
324        "in-list character string",
325        structure(list(name = "p",
326          attribs = list(),
327          children = list()),
328          class = "shiny.tag"
329        ),
330        structure(list(name = "p",
331          attribs = list(),
332          children = list("in-list child tag 2")),
333          class = "shiny.tag"
334        ),
335        "character string",
336        "1234"
337      )
338    ),
339    class = "shiny.tag"
340  )
341
342  expect_identical(renderTags(t1)$html, renderTags(t1_full)$html)
343})
344
345# The .noWS option was added in 0.3.6.9003; we may still encounter tags created
346# in an older version (perhaps saved to an RDS file and restored). They would
347# lack this element in their structure.
348test_that("Old tags without the .noWS option can still be rendered", {
349  oldTag <- structure(
350    list(name = "div", attribs = dots_list(), children = list("text")),
351    .Names = c("name", "attribs", "children"),
352    class = "shiny.tag"
353  )
354  w <- WSTextWriter()
355  tagWrite(oldTag, w)
356
357  expect_identical(
358    w$readAll(),
359    "<div>text</div>\n"
360  )
361})
362
363# We moved to rlang::dots_list in 0.3.6; we may still encounter tags created
364# in an older version (perhaps saved to an RDS file and restored). They would
365# use old-school lists.
366test_that("Old tags predating rlang::list2 can still be rendered", {
367  oldTag <- structure(
368    list(name = "div", attribs = list(), children = list("text")),
369    .Names = c("name", "attribs", "children"),
370    class = "shiny.tag"
371  )
372  w <- WSTextWriter()
373  tagWrite(oldTag, w)
374
375  expect_identical(
376    w$readAll(),
377    "<div>text</div>\n"
378  )
379})
380
381test_that("tag with noWS works",{
382  oneline <- tag("span", list(tag("strong", "Super strong", .noWS="outside")))
383  expect_identical(as.character(oneline), "<span><strong>Super strong</strong></span>")
384})
385
386test_that("tag/s with invalid noWS fails fast", {
387  expect_error(tag("span", .noWS="wrong"))
388  expect_error(tags$a(.noWS="wrong"))
389})
390
391test_that("Attributes are preserved", {
392  # HTML() adds an attribute to the data structure (note that this is
393  # different from the 'attribs' field in the list)
394  x <- HTML("<tag>&&</tag>")
395  expect_identical(attr(x, "html", TRUE), TRUE)
396  expect_equivalent(format(x), "<tag>&&</tag>")
397
398  # Make sure attributes are preserved when wrapped in other tags
399  x <- div(HTML("<tag>&&</tag>"))
400  expect_equivalent(x$children[[1]], HTML("<tag>&&</tag>"))
401  expect_identical(attr(x$children[[1]], "html", TRUE), TRUE)
402  expect_equivalent(format(x), "<div><tag>&&</tag></div>")
403
404  # Deeper nesting
405  x <- div(p(HTML("<tag>&&</tag>")))
406  expect_equivalent(x$children[[1]]$children[[1]], HTML("<tag>&&</tag>"))
407  expect_identical(attr(x$children[[1]]$children[[1]], "html", TRUE), TRUE)
408  expect_equivalent(format(x), "<div>\n  <p><tag>&&</tag></p>\n</div>")
409})
410
411test_that("Adding attributes to tags", {
412  t1 <- tags$div("foo")
413
414  # Adding attributes to empty tag
415  expect_identical(t1$attribs, dots_list())
416  expect_identical(
417    tagAppendAttributes(t1, class = "c1")$attribs,
418    list(class = "c1")
419  )
420
421  # Adding attribute with multiple values
422  expect_identical(
423    tagAppendAttributes(t1, class = "c1 c2")$attribs,
424    list(class = "c1 c2")
425  )
426
427  # Adding two different attributes
428  expect_identical(
429    tagAppendAttributes(t1, class = "c1", id = "foo")$attribs,
430    list(class = "c1", id = "foo")
431  )
432
433  # Adding attributes in two successive calls
434  expect_identical(
435    tagAppendAttributes(
436      tagAppendAttributes(t1, class = "c1 c2"), class = "c3")$attribs,
437    list(class = "c1 c2", class = "c3")
438  )
439
440  # Adding empty attributes
441  expect_identical(
442    tagAppendAttributes(t1, class = NULL)$attribs,
443    list()
444  )
445  expect_identical(
446    tagAppendAttributes(
447      tagAppendAttributes(t1, class = "hidden"), class = NULL)$attribs,
448    list(class = "hidden")
449  )
450
451  t2 <- tags$div("foo", class = "c1")
452
453  # Adding attributes on a tag with other attributes
454  expect_identical(
455    tagAppendAttributes(t2, id = "foo")$attribs,
456    list(class = "c1", id = "foo")
457  )
458
459  # Adding attributes on a tag with the same attribute
460  expect_identical(
461    tagAppendAttributes(t2, class = "c2")$attribs,
462    list(class = "c1", class = "c2")
463  )
464})
465
466test_that("Adding unnamed attributes creates a warning", {
467  expect_error(
468    tagAppendAttributes(
469      tags$div(),
470      "value"
471    ),
472    "include an attribute name"
473  )
474
475  x <- div()
476  x$attribs[[1]] <- "value"
477  expect_error(
478    print(x),
479    "name all of your attribute values"
480  )
481})
482
483
484
485test_that("Testing for attributes on tags", {
486  t1 <- tags$div("foo", class = "c1", class = "c2", id = "foo")
487
488  # Testing for attribute that does not exist
489  expect_identical(
490    tagHasAttribute(t1, "nope"),
491    FALSE
492  )
493
494  # Testing for an attribute that exists once
495  expect_identical(
496    tagHasAttribute(t1, "id"),
497    TRUE
498  )
499
500  # Testing for an attribute that exists multiple times
501  expect_identical(
502    tagHasAttribute(t1, "class"),
503    TRUE
504  )
505
506  # Testing for substring of an attribute that exists
507  expect_identical(
508    tagHasAttribute(t1, "clas"),
509    FALSE
510  )
511
512  # Testing for superstring of an attribute that exists
513  expect_identical(
514    tagHasAttribute(t1, "classes"),
515    FALSE
516  )
517
518  # Testing for attribute with empty value
519  t2 <- tags$div("foo", foo = "")
520  expect_identical(
521    tagHasAttribute(t2, "foo"),
522    TRUE
523  )
524
525  # Testing for attribute with NULL value
526  t3 <- tags$div("foo", foo = NULL)
527  expect_identical(
528    tagHasAttribute(t3, "foo"),
529    FALSE
530  )
531})
532
533test_that("Getting attributes from tags", {
534  # Getting an attribute from a tag with no attributes
535  t1 <- tags$div("foo")
536  expect_identical(
537    tagGetAttribute(t1, "class"),
538    NULL
539  )
540
541  t2 <- tags$div("foo", class = "c1")
542
543  # Getting an attribute from a tag without the correct attribute
544  expect_identical(
545    tagGetAttribute(t2, "id"),
546    NULL
547  )
548
549  # Getting an attribute from a tag with the a single value for the attribute
550  expect_identical(
551    tagGetAttribute(t2, "class"),
552    "c1"
553  )
554
555  # Getting an attribute from a tag with multiple matching attributes
556  t3 <- tags$div("foo", class = "c1", id = "foo", class = "c2")
557  expect_identical(
558    tagGetAttribute(t3, "class"),
559    "c1 c2"
560  )
561
562  # Getting an attribute from a tag where the attributes were factors
563  t4 <- tags$div("foo", class = as.factor("c1"), class = as.factor("c2"))
564  expect_identical(
565    tagGetAttribute(t4, "class"),
566    "c1 c2"
567  )
568
569  # Getting a numeric attribute from a tag
570  t5 <- tags$div("foo", class = 78)
571  expect_identical(
572    tagGetAttribute(t5, "class"),
573    "78"
574  )
575})
576
577test_that("NA attributes are rendered correctly", {
578  expect_identical(
579    as.character(tags$div("text", foo = NA)),
580    '<div foo>text</div>'
581  )
582  expect_identical(
583    as.character(tags$div("text", class = "a", foo = NA)),
584    '<div class="a" foo>text</div>'
585  )
586  expect_identical(
587    as.character(tags$div("text", class = "a", foo = NA, class = "b")),
588    '<div class="a b" foo>text</div>'
589  )
590
591  # Multiple NA's are coalesced
592  expect_identical(
593    as.character(tags$div("text", class = "a", foo = NA, class = "b", foo = NA)),
594    '<div class="a b" foo>text</div>'
595  )
596
597  # A non-NA value supersedes NA
598  expect_identical(
599    as.character(tags$div("text", class = "a", foo = NA, foo = "b")),
600    '<div class="a" foo="b">text</div>'
601  )
602  expect_identical(
603    as.character(tags$div("text", class = "a", foo = "b", foo = NA, foo = "c")),
604    '<div class="a" foo="b c">text</div>'
605  )
606  expect_identical(
607    as.character(tags$div("text", class = "a", foo = "b", foo = NA, foo = NA, foo = "c")),
608    '<div class="a" foo="b c">text</div>'
609  )
610})
611
612test_that("NA attributes are retrieved correctly", {
613  expect_foo_attr <- function(y, ...) {
614    testTag <- tags$div("text", ...)
615    expect_identical(
616      tagGetAttribute(testTag, "foo"),
617      y
618    )
619  }
620  expect_foo_attr(NA, foo = NA)
621  expect_foo_attr(NA, class = "a", foo = NA)
622  expect_foo_attr(NA, class = "a", foo = NA, class = "b")
623
624  # Multiple NA's are coalesced
625  expect_foo_attr(NA, class = "a", foo = NA, class = "b", foo = NA)
626
627  # A non-NA value supersedes NA
628  expect_foo_attr("b", class = "a", foo = NA, foo = "b")
629  expect_foo_attr("b c", class = "a", foo = "b", foo = NA, foo = "c")
630  expect_foo_attr("b c", class = "a", foo = "b", foo = NA, foo = NA, foo = "c")
631
632  # Non atomic value cause a list to be returned.
633  expect_foo_attr(list(list("b")), class = "a", foo = NA, foo = list("b"))
634  expect_foo_attr(list(list("b"), list("c")), class = "a", foo = list("b"), foo = NA, foo = list("c"))
635  expect_foo_attr(list("b", list("c")), class = "a", foo = "b", foo = NA, foo = NA, foo = list("c"))
636})
637
638test_that("Tag list tree is rendered in DOM tree order", {
639  # Tree order is preorder, depth-first traversal
640  # https://dom.spec.whatwg.org/#concept-tree
641  #
642  # Test for preordered traversal/execution of tagFunction(). This allows one to
643  # rely on the side-effects of executing a tag, so long as those side-effects
644  # happen "towards the top" of the tree. Shiny implicitly assumes this
645  # behavior: execution of bootstrapLib() introduces a (temporary) side-effect
646  # that "down-stream" UI (i.e. sliderInput() et al) can use to inform their
647  # Sass -> CSS compilation
648  value <- NULL
649  lazyDiv <- div(tagFunction(function() { value }))
650  dom <- tagList(
651    lazyDiv,
652    div(tagList(
653      tagFunction(function() { value <<- 1 })
654    )),
655    lazyDiv
656  )
657  expect_identical(
658    as.character(dom),
659    "<div></div>\n<div>1</div>\n<div>1</div>"
660  )
661})
662
663
664test_that("Flattening a list of tags", {
665  # Flatten a nested list
666  nested <- list(
667    "a1",
668    list(
669      "b1",
670      list("c1", "c2"),
671      list(),
672      "b2",
673      list("d1", "d2")
674    ),
675    "a2"
676  )
677
678  flat <- list("a1", "b1", "c1", "c2", "b2", "d1", "d2", "a2")
679  expect_identical(flattenTags(nested), flat)
680
681  # no-op for flat lists
682  expect_identical(flattenTags(list(a="1", "b")), list(a="1", "b"))
683
684  # numbers are coerced to character
685  expect_identical(flattenTags(list(a=1, "b")), list(a="1", "b"))
686
687  # empty list results in empty list
688  expect_identical(flattenTags(list()), list())
689
690  # preserve attributes
691  nested <- list("txt1", list(structure("txt2", prop="prop2")))
692  flat <- list("txt1",
693    structure("txt2", prop="prop2"))
694  expect_identical(flattenTags(nested), flat)
695})
696
697test_that("Head and singleton behavior", {
698  result <- renderTags(tagList(
699    tags$head(singleton("hello"))
700  ))
701
702  expect_identical(result$html, HTML(""))
703  expect_identical(result$head, HTML("  hello"))
704  expect_identical(result$singletons, "089cce0335cf2bae2bcb08cc753ba56f8e1ea8ed")
705
706  # Ensure that "hello" actually behaves like a singleton
707  result2 <- renderTags(tagList(
708    tags$head(singleton("hello"))
709  ), singletons = result$singletons)
710
711  expect_identical(result$singletons, result2$singletons)
712  expect_identical(result2$head, HTML(""))
713  expect_identical(result2$html, HTML(""))
714
715  result3 <- renderTags(tagList(
716    tags$head(singleton("hello"), singleton("hello"))
717  ))
718  expect_identical(result$singletons, result3$singletons)
719  expect_identical(result3$head, HTML("  hello"))
720
721  # Ensure that singleton can be applied to lists, not just tags
722  result4 <- renderTags(list(singleton(list("hello")), singleton(list("hello"))))
723  expect_identical(result4$singletons, "110d1f0ef6762db2c6863523a7c379a697b43ea3")
724  expect_identical(result4$html, renderTags(HTML("hello"))$html)
725
726  result5 <- renderTags(tagList(singleton(list(list("hello")))))
727  expect_identical(result5$html, renderTags("hello")$html)
728})
729
730test_that("Factors are treated as characters, not numbers", {
731  myfactors <- factor(LETTERS[1:3])
732  expect_identical(
733    as.character(tags$option(value=myfactors[[1]], myfactors[[1]])),
734    '<option value="A">A</option>'
735  )
736
737  expect_identical(
738    as.character(tags$option(value=myfactors[[1]], value='B', value=3, myfactors[[1]])),
739    '<option value="A B 3">A</option>'
740  )
741})
742
743test_that("Unusual list contents are rendered correctly", {
744  expect_identical(renderTags(list(NULL)), renderTags(HTML("")))
745  expect_identical(renderTags(list(100)), renderTags(HTML("100")))
746  expect_identical(renderTags(list(list(100))), renderTags(HTML("100")))
747  expect_identical(renderTags(list(list())), renderTags(HTML("")))
748  expect_identical(renderTags(NULL), renderTags(HTML("")))
749})
750
751test_that("Low-level singleton manipulation methods", {
752  # Default arguments drop singleton duplicates and strips the
753  # singletons it keeps of the singleton bit
754  result1 <- takeSingletons(tags$div(
755    singleton(tags$head(tags$script("foo"))),
756    singleton(tags$head(tags$script("foo")))
757  ))
758
759  expect_identical(result1$ui$children[[2]], NULL)
760  expect_false(is.singleton(result1$ui$children[[1]]))
761
762  # desingleton=FALSE means drop duplicates but don't strip the
763  # singleton bit
764  result2 <- takeSingletons(tags$div(
765    singleton(tags$head(tags$script("foo"))),
766    singleton(tags$head(tags$script("foo")))
767  ), desingleton=FALSE)
768
769  expect_identical(result2$ui$children[[2]], NULL)
770  expect_true(is.singleton(result2$ui$children[[1]]))
771
772  result3 <- surroundSingletons(tags$div(
773    singleton(tags$script("foo")),
774    singleton(tags$script("foo"))
775  ))
776
777  expect_identical(
778    renderTags(result3)$html,
779    HTML("<div>
780  <!--SHINY.SINGLETON[98eee9ba1f9e4ab3db75f33036bf91d4e214342b]-->
781  <script>foo</script>
782  <!--/SHINY.SINGLETON[98eee9ba1f9e4ab3db75f33036bf91d4e214342b]-->
783  <!--SHINY.SINGLETON[98eee9ba1f9e4ab3db75f33036bf91d4e214342b]-->
784  <script>foo</script>
785  <!--/SHINY.SINGLETON[98eee9ba1f9e4ab3db75f33036bf91d4e214342b]-->
786</div>")
787    )
788})
789
790test_that("Indenting can be controlled/suppressed", {
791  expect_identical(
792    renderTags(tags$div("a", "b"))$html,
793    HTML("<div>\n  a\n  b\n</div>")
794  )
795  expect_identical(
796    format(tags$div("a", "b")),
797    "<div>\n  a\n  b\n</div>"
798  )
799
800  expect_identical(
801    renderTags(tags$div("a", "b"), indent = 2)$html,
802    HTML("    <div>\n      a\n      b\n    </div>")
803  )
804  expect_identical(
805    format(tags$div("a", "b"), indent = 2),
806    "    <div>\n      a\n      b\n    </div>"
807  )
808
809  expect_identical(
810    renderTags(tags$div("a", "b"), indent = FALSE)$html,
811    HTML("<div>\na\nb\n</div>")
812  )
813  expect_identical(
814    format(tags$div("a", "b"), indent = FALSE),
815    "<div>\na\nb\n</div>"
816  )
817
818  expect_identical(
819    renderTags(tagList(tags$div("a", "b")), indent = FALSE)$html,
820    HTML("<div>\na\nb\n</div>")
821  )
822  expect_identical(
823    format(tagList(tags$div("a", "b")), indent = FALSE),
824    "<div>\na\nb\n</div>"
825  )
826})
827
828test_that("cssList tests", {
829  expect_identical(NULL, css())
830  expect_identical(NULL, css())
831  expect_identical(
832    css(
833      font.family = 'Helvetica, "Segoe UI"',
834      font_size = "12px",
835      `font-style` = "italic",
836      font.variant = NULL,
837      "font-weight!" = factor("bold"),
838      padding = c("10px", "9px", "8px")
839    ),
840    "font-family:Helvetica, \"Segoe UI\";font-size:12px;font-style:italic;font-weight:bold !important;padding:10px 9px 8px;"
841  )
842
843  # Unnamed args not allowed
844  expect_error(css("10"))
845  expect_error(css(1, b=2))
846
847  # NULL and empty string are dropped
848  expect_null(css(a="", b = NULL, "c!" = NULL, d = character()))
849
850  # We are dumb about duplicated properties. Probably don't do that.
851  expect_identical(css(a=1, a=2), "a:1;a:2;")
852})
853
854test_that("Non-tag objects can be coerced", {
855
856  .GlobalEnv$as.tags.testcoerce1 <- function(x) {
857    list(singleton(list("hello")))
858  }
859  on.exit(rm("as.tags.testcoerce1", pos = .GlobalEnv), add = TRUE)
860
861  # Make sure tag-coerceable objects are tagified
862  result1 <- renderTags(structure(TRUE, class = "testcoerce1"))
863  expect_identical(result1$html, HTML("hello"))
864  expect_identical(result1$singletons, "110d1f0ef6762db2c6863523a7c379a697b43ea3")
865
866  # Make sure tag-coerceable objects are tagified before singleton handling
867  # occurs, but that over-flattening doesn't happen
868  result2 <- renderTags(tagList(
869    singleton(list("hello")),
870    structure(TRUE, class = "testcoerce1")
871  ))
872  expect_identical(result2$html, HTML("hello"))
873  expect_identical(result2$singletons, "110d1f0ef6762db2c6863523a7c379a697b43ea3")
874
875})
876
877test_that("Latin1 and system encoding are converted to UTF-8", {
878  #Sys.setlocale(, "Chinese")
879  latin1_str <- rawToChar(as.raw(0xFF))
880  Encoding(latin1_str) <- "latin1"
881
882  divLatin1 <- as.character(tags$div(latin1_str))
883  expect_identical(
884    charToRaw(divLatin1),
885    as.raw(c(0x3c, 0x64, 0x69, 0x76, 0x3e, 0xc3, 0xbf, 0x3c, 0x2f,
886      0x64, 0x69, 0x76, 0x3e))
887  )
888  expect_identical(Encoding(divLatin1), "UTF-8")
889
890  expect_identical(Encoding("\u4E11"), "UTF-8")
891  divUTF8 <- as.character(tags$div("\u4E11"))
892  expect_identical(
893    charToRaw(divUTF8),
894    as.raw(c(0x3c, 0x64, 0x69, 0x76, 0x3e, 0xe4, 0xb8, 0x91, 0x3c,
895      0x2f, 0x64, 0x69, 0x76, 0x3e))
896  )
897  expect_identical(Encoding(divUTF8), "UTF-8")
898
899  divMixed <- format(tags$div(
900    "\u4E11", latin1_str,
901    tags$span(a="\u4E11", latin1_str),
902    tags$span(b=latin1_str, HTML("\u4E11"))
903  ))
904  expect_identical(
905    charToRaw(divMixed),
906    as.raw(c(0x3c, 0x64, 0x69, 0x76, 0x3e, 0x0a, 0x20, 0x20, 0xe4,
907      0xb8, 0x91, 0x0a, 0x20, 0x20, 0xc3, 0xbf, 0x0a, 0x20, 0x20, 0x3c,
908      0x73, 0x70, 0x61, 0x6e, 0x20, 0x61, 0x3d, 0x22, 0xe4, 0xb8, 0x91,
909      0x22, 0x3e, 0xc3, 0xbf, 0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, 0x3e,
910      0x0a, 0x20, 0x20, 0x3c, 0x73, 0x70, 0x61, 0x6e, 0x20, 0x62, 0x3d,
911      0x22, 0xc3, 0xbf, 0x22, 0x3e, 0xe4, 0xb8, 0x91, 0x3c, 0x2f, 0x73,
912      0x70, 0x61, 0x6e, 0x3e, 0x0a, 0x3c, 0x2f, 0x64, 0x69, 0x76, 0x3e
913    ))
914  )
915  expect_identical(Encoding(divMixed), "UTF-8")
916
917  # Encoding(HTML(latin1_str)) is "UTF-8" on Linux; even just
918  # paste(latin1_str) returns a UTF-8 encoded string
919  #expect_identical(Encoding(HTML(latin1_str)), "latin1")
920
921  expect_identical(Encoding(format(HTML(latin1_str))), "UTF-8")
922  expect_identical(Encoding(format(tagList(latin1_str))), "UTF-8")
923
924  # ensure the latin1 attribute returns correctly after escaping
925  latin1_str2 <- rawToChar(as.raw(c(0xff, 0x0d, 0x0a)))
926  Encoding(latin1_str2) <- "latin1"
927  spanLatin <- as.character(tags$span(latin1_str2, title = latin1_str2))
928  expect_identical(Encoding(spanLatin), "UTF-8")
929  expect_identical(
930    charToRaw(spanLatin),
931    as.raw(c(0x3c, 0x73, 0x70, 0x61, 0x6e, 0x20, 0x74, 0x69, 0x74,
932             0x6c, 0x65, 0x3d, 0x22, 0xc3, 0xbf, 0x26, 0x23, 0x31, 0x33, 0x3b,
933             0x26, 0x23, 0x31, 0x30, 0x3b, 0x22, 0x3e, 0xc3, 0xbf, 0x0d, 0x0a,
934             0x3c, 0x2f, 0x73, 0x70, 0x61, 0x6e, 0x3e
935    ))
936  )
937})
938
939test_that("paste8 in Chinese locale works", {
940  loc <- "Chinese"
941  testthat::skip_if_not(is_locale_available(loc), "Chinese locale not available")
942
943  withr::with_locale(c(LC_COLLATE=loc, LC_CTYPE=loc, LC_MONETARY=loc, LC_TIME=loc), {
944    x <- "\377"
945    Encoding(x) <- "latin1"
946    expect_identical(x, "\Uff")
947    expect_identical(Encoding(x), "latin1")
948
949    y <- "\U4E2d"  # Using \Uxxxx always is encoded as UTF-8
950    expect_identical(y, "\U4E2d")
951    expect_identical(Encoding(y), "UTF-8")
952
953    xy <- paste8(x, y)
954    xy
955    expect_identical(xy, "\Uff \U4E2d")
956    expect_identical(Encoding(xy), "UTF-8")
957
958    xy <- paste8(c(x, y), collapse = "")
959    expect_identical(xy, "\Uff\U4E2d")
960    expect_identical(Encoding(xy), "UTF-8")
961  })
962})
963
964test_that("Printing tags works", {
965  expect_identical(
966    capture.output(print(tags$a(href = "#", "link"))),
967    '<a href="#">link</a>'
968  )
969})
970
971test_that("htmlEscape will try to coerce inputs to characters", {
972  x <- list(a1 = "b", a2 = list("b1", "b2"))
973  expect_identical(
974    htmlEscape(x),
975    as.character(x)
976  )
977})
978
979test_that("trailing commas allowed everywhere", {
980  expect_silent({
981    t1 <- div("foo",)
982    tagList(t1,)
983    tagSetChildren(t1, "child",)
984    tagAppendAttributes(t1, class = "bar",)
985    tagAppendChildren(t1, "child2",)
986    css(style = "",)
987  })
988})
989
990test_that("extractPreserveChunks works for emoji strings", {
991  # solaris doesn't seem to support Unicode characters with surrogate pairs
992  # (just by creating such a string will cause a warning)
993  # > "\U0001F937"
994  # [1] "\U0001f937"
995  # Warning message:
996  #   it is not known that wchar_t is Unicode on this platform
997  skip_on_os("solaris")
998  x <- "<!--html_preserve-->chunk1<!--/html_preserve-->\U0001F937<!--html_preserve-->chunk2<!--/html_preserve-->"
999  out <- extractPreserveChunks(x)
1000  expect_equivalent(
1001    out$chunks,
1002    c('chunk2', 'chunk1')
1003  )
1004})
1005
1006
1007test_that("complicated class attributes are handled", {
1008  x <- div(class = as.factor(letters)[1], class = "b c", class = c("d", "e f"))
1009  expect_equal(
1010    tagGetAttribute(x, "class"),
1011    "a b c d e f"
1012  )
1013  expect_identical(
1014    as.character(x),
1015    "<div class=\"a b c d e f\"></div>"
1016  )
1017})
1018
1019
1020test_that("html render method", {
1021  local_edition(3)
1022
1023  # Have a place holder div and return a span instead
1024  obj <- div("example", .renderHook = function(x) {
1025    x$name <- "span"
1026    x
1027  })
1028  expect_equal(obj$name, "div")
1029  expect_snapshot(as.character(obj))
1030
1031  # Add a class to the tag
1032  spanExtra <- tagAddRenderHook(obj, function(x) {
1033    tagAppendAttributes(x, class = "extra")
1034  })
1035  expect_equal(spanExtra$name, "div")
1036  expect_equal(spanExtra$attribs$class, NULL)
1037  expect_snapshot(as.character(spanExtra))
1038
1039  # Replace the previous render method
1040  # Should print a `div` with class `"extra"`
1041  divExtra <- tagAddRenderHook(obj, replace = TRUE, function(x) {
1042    tagAppendAttributes(x, class = "extra")
1043  })
1044  expect_equal(divExtra$attribs$class, NULL)
1045  expect_snapshot(as.character(divExtra))
1046
1047  # Add more child tags
1048  spanExtended <- tagAddRenderHook(obj, function(x) {
1049    tagAppendChildren(x, tags$strong("bold text"))
1050  })
1051  expect_equal(spanExtended$name, "div")
1052  expect_equal(spanExtended$children, obj$children)
1053  expect_snapshot(as.character(spanExtended))
1054
1055  tagFuncExt <- tagAddRenderHook(obj, function(x) {
1056    tagFunction(function() tagList(x, tags$p("test")) )
1057  })
1058  expect_equal(tagFuncExt$name, "div")
1059  expect_equal(tagFuncExt$children, obj$children)
1060  expect_snapshot(as.character(tagFuncExt))
1061
1062  # Add a new html dependency
1063  newDep <- tagAddRenderHook(obj, function(x) {
1064    fa <- htmlDependency(
1065      "font-awesome", "4.5.0", c(href="shared/font-awesome"),
1066      stylesheet = "css/font-awesome.min.css")
1067    attachDependencies(x, fa, append = TRUE)
1068  })
1069  # Also add a jqueryui html dependency
1070  htmlDependencies(newDep) <- htmlDependency(
1071    "jqueryui", "1.11.4", c(href="shared/jqueryui"),
1072    script = "jquery-ui.min.js")
1073  expect_equal(newDep$name, "div")
1074  expect_length(htmlDependencies(newDep), 1)
1075  expect_snapshot(renderTags(newDep))
1076
1077  # Ignore the original tag and return something completely new.
1078  newObj <- tagAddRenderHook(obj, function(x) {
1079    tags$p("Something else")
1080  })
1081  expect_equal(newObj$name, "div")
1082  expect_snapshot(as.character(newObj))
1083})
1084
1085
1086test_that(".cssSelector arg only applies changes to the selected elements", {
1087  html <-
1088    div(
1089      class = "outer",
1090      div(class = "inner", "text"),
1091      span("TEXT")
1092    )
1093
1094  expect_equal_tags(
1095    tagAppendAttributes(html, id = "test"),
1096    div(class = "outer", id = "test", div(class="inner", "text"), span("TEXT"))
1097  )
1098  expect_equal_tags(
1099    tagAppendAttributes(html, id = "test", .cssSelector = ".inner"),
1100    div(class = "outer", div(class = "inner", id = "test", "text"), span("TEXT"))
1101  )
1102
1103  expect_equal_tags(
1104    tagAppendChild(html, h1()),
1105    div(class = "outer", div(class="inner", "text"), span("TEXT"), h1())
1106  )
1107  expect_equal_tags(
1108    tagAppendChild(html, h1(), .cssSelector = ".inner"),
1109    div(class = "outer", div(class = "inner", "text", h1()), span("TEXT"))
1110  )
1111
1112  expect_equal_tags(
1113    tagAppendChildren(html, h1(), h2()),
1114    div(class = "outer", div(class="inner", "text"), span("TEXT"), h1(), h2())
1115  )
1116  expect_equal_tags(
1117    tagAppendChildren(html, h1(), h2(), .cssSelector = ".inner"),
1118    div(class = "outer", div(class = "inner", "text", h1(), h2()), span("TEXT"))
1119  )
1120
1121  expect_equal_tags(
1122    tagSetChildren(html, h1(), h2()),
1123    div(class = "outer", h1(), h2())
1124  )
1125  expect_equal_tags(
1126    tagSetChildren(html, h1(), h2(), .cssSelector = ".inner"),
1127    div(class = "outer", div(class = "inner", h1(), h2()), span("TEXT"))
1128  )
1129
1130  expect_equal_tags(
1131    tagInsertChildren(html, h1(), h2(), after = 0),
1132    div(class = "outer", h1(), h2(), div(class="inner", "text"), span("TEXT"))
1133  )
1134  expect_equal_tags(
1135    tagInsertChildren(html, h1(), h2(), after = 0, .cssSelector = ".inner"),
1136    div(class = "outer", div(class = "inner", h1(), h2(), "text"), span("TEXT"))
1137  )
1138})
1139
1140
1141
1142
1143test_that("flattenTagAttribs", {
1144  attribs <- list(
1145    b = "1",
1146    a = "2",
1147    b = "3"
1148  )
1149
1150  flatAttribs <- flattenTagAttribs(attribs)
1151  # alpha sorted
1152  expect_equal(names(flatAttribs), c("a", "b"))
1153  # b values are collected
1154  expect_equal(flatAttribs, list(a = "2", b = c("1", "3")))
1155})
1156