| Marc Kupietz | b066e74 | 2026-08-23 09:57:06 +0300 | [diff] [blame] | 1 | test_that("parse_duration_seconds handles all duration formats", { |
| 2 | expect_equal(parse_duration_seconds("00:30"), 30) |
| 3 | expect_equal(parse_duration_seconds("1:05"), 65) |
| 4 | expect_equal(parse_duration_seconds("45"), 45) |
| 5 | expect_equal(parse_duration_seconds("1:00:00"), 3600) |
| 6 | expect_equal(parse_duration_seconds("1:30:15"), 5415) |
| 7 | expect_true(is.na(parse_duration_seconds("abc"))) |
| 8 | }) |
| 9 | |
| 10 | test_that("extract_timing_comments finds comments", { |
| 11 | lines <- c( |
| 12 | "---", |
| 13 | "title: x", |
| 14 | "---", |
| 15 | "", |
| 16 | "## Slide one", |
| 17 | "", |
| 18 | "<!-- MK 00:30 -->", |
| 19 | "", |
| 20 | "## Slide two", |
| 21 | "", |
| 22 | "<!-- AB 1:00 -->", |
| 23 | "<!-- MK 90 -->" |
| 24 | ) |
| 25 | comments <- extract_timing_comments(lines) |
| 26 | expect_equal(comments$speaker, c("MK", "AB", "MK")) |
| 27 | expect_equal(comments$seconds, c(30, 60, 90)) |
| 28 | expect_equal(assign_slides(lines, comments, slide_level = 2), |
| 29 | c("Slide one", "Slide two", "Slide two")) |
| 30 | }) |
| 31 | |
| 32 | test_that("comments without speaker codes are supported", { |
| 33 | lines <- c("## Slide one", "<!-- 00:30 -->", "## Slide two", "<!-- 1:05 -->") |
| 34 | comments <- extract_timing_comments(lines) |
| 35 | expect_true(all(is.na(comments$speaker))) |
| 36 | expect_equal(comments$seconds, c(30, 65)) |
| 37 | |
| 38 | # a plain number must not be split into a speaker code plus duration |
| 39 | comments2 <- extract_timing_comments("<!-- 45 -->") |
| 40 | expect_true(is.na(comments2$speaker)) |
| 41 | expect_equal(comments2$seconds, 45) |
| 42 | |
| 43 | result <- apply_timing_attributes(c( |
| 44 | "<section id='s1' class='level2'>", |
| 45 | "<!-- 00:30 -->", |
| 46 | "</section>" |
| 47 | )) |
| 48 | expect_match(result[1], "data-timing=\"30\"") |
| 49 | |
| 50 | report <- timing_report_message(extract_timing_comments(lines)) |
| 51 | expect_equal(report, "Slide timing -- total 1:35") |
| 52 | }) |
| 53 | |
| 54 | test_that("comments before first heading belong to title slide", { |
| 55 | lines <- c("<!-- MK 00:10 -->", "", "# Title", "", "## Slide") |
| 56 | comments <- extract_timing_comments(lines) |
| 57 | expect_equal(assign_slides(lines, comments, slide_level = 2), "(title)") |
| 58 | }) |
| 59 | |
| 60 | test_that("apply_timing_attributes adds data-timing to sections", { |
| 61 | lines <- c( |
| 62 | "<html>", |
| 63 | "<section id='title-slide'>", |
| 64 | "title", |
| 65 | "</section>", |
| 66 | "<section id='s1' class='level2'>", |
| 67 | "<!-- MK 00:30 -->", |
| 68 | "</section>", |
| 69 | "<section id='s2' class='level2'>", |
| 70 | "<!-- MK 0:20 -->", |
| 71 | "<!-- AB 00:30 -->", |
| 72 | "</section>" |
| 73 | ) |
| 74 | result <- apply_timing_attributes(lines) |
| 75 | html <- paste(result, collapse = "\n") |
| 76 | expect_match(result[2], "<section id='title-slide'>") |
| 77 | expect_match(result[5], "data-timing=\"30\"") |
| 78 | # multiple speakers on one slide are summed |
| 79 | expect_match(result[8], "data-timing=\"50\"") |
| 80 | comments <- attr(result, "comments") |
| 81 | expect_equal(sum(comments$seconds), 80) |
| 82 | }) |
| 83 | |
| 84 | test_that("apply_timing_attributes is a no-op without comments", { |
| 85 | lines <- c("<section id='s1'>", "x", "</section>") |
| 86 | result <- apply_timing_attributes(lines) |
| 87 | expect_identical(as.vector(result), lines) |
| 88 | expect_equal(nrow(attr(result, "comments")), 0) |
| 89 | }) |
| 90 | |
| 91 | test_that("inject_total_time activates the pacing timer", { |
| 92 | lines <- c( |
| 93 | "var opts = {", |
| 94 | " controls: true,", |
| 95 | "};", |
| 96 | "Reveal.initialize({", |
| 97 | " controls: true,", |
| 98 | " slideNumber: true", |
| 99 | "});" |
| 100 | ) |
| 101 | result <- inject_total_time(lines, 90) |
| 102 | expect_contains(result, "\t\t\ttotalTime: 90,") |
| 103 | |
| 104 | # existing pacing config is respected |
| 105 | with_default <- c(lines, "", "defaultTiming: 60") |
| 106 | expect_identical(inject_total_time(with_default, 90), with_default) |
| 107 | with_total <- c("totalTime: 120", lines) |
| 108 | expect_identical(inject_total_time(with_total, 90), with_total) |
| 109 | |
| 110 | # nothing to hook into -- leave the document alone |
| 111 | expect_identical(inject_total_time(c("<html>", "</html>"), 90), |
| 112 | c("<html>", "</html>")) |
| 113 | }) |
| 114 | |
| 115 | test_that("rendered presentations carry data-timing attributes", { |
| 116 | skip_if_not_pandoc() |
| 117 | skip_if_not_installed("xml2") |
| 118 | rmd <- local_temp_rmd_file( |
| 119 | "---", |
| 120 | "title: Timing test", |
| 121 | "output: revealjs.ids::revealjs_presentation", |
| 122 | "---", |
| 123 | "", |
| 124 | "## Slide A", |
| 125 | "", |
| 126 | "Content A", |
| 127 | "", |
| 128 | "<!-- MK 00:30 -->", |
| 129 | "", |
| 130 | "## Slide B", |
| 131 | "", |
| 132 | "Content B", |
| 133 | "", |
| 134 | "<!-- MK 01:00 -->", |
| 135 | "<!-- AB 0:30 -->" |
| 136 | ) |
| 137 | html <- .render_and_read(rmd) |
| 138 | timings <- xml2::xml_attr( |
| 139 | xml2::xml_find_all(html, "//section[@data-timing]"), |
| 140 | "data-timing" |
| 141 | ) |
| 142 | expect_equal(timings, c("30", "90")) |
| 143 | # the grand total activates the pacing timer of the notes plugin |
| 144 | expect_true(any(grepl("totalTime:\\s*120", html))) |
| 145 | }) |
| 146 | |
| Marc Kupietz | 6b65c03 | 2026-08-25 09:43:28 +0300 | [diff] [blame^] | 147 | test_that("title slide notes and timing are merged into the title section", { |
| 148 | lines <- c( |
| 149 | '<section class="title-frame" id="title-slide">', |
| 150 | "<h1 class=\"title\">Title</h1>", |
| 151 | "</section>", |
| 152 | "<section class=\"slide level2\">", |
| 153 | "<!-- JD 00:30 -->", |
| 154 | "<aside class=\"notes\">", |
| 155 | "<p>Welcome</p>", |
| 156 | "</aside>", |
| 157 | "</section>", |
| 158 | "<section id=\"slide-a\" class=\"slide level2\">", |
| 159 | "<!-- MK 01:00 -->", |
| 160 | "</section>" |
| 161 | ) |
| 162 | merged <- merge_title_slide_notes(lines) |
| 163 | # phantom slide (6 lines) removed, its 4 content lines moved into the title |
| 164 | expect_length(merged, length(lines) - 2) |
| 165 | expect_identical(merged[1], lines[1]) |
| 166 | expect_match(paste(merged[1:5], collapse = "\n"), "Welcome") |
| 167 | expect_match(paste(merged[1:5], collapse = "\n"), "JD 00:30") |
| 168 | # only the real Slide A section remains |
| 169 | expect_equal(sum(grepl('class="slide level2"', merged)), 1) |
| 170 | |
| 171 | # a phantom slide with real content is left alone |
| 172 | with_content <- lines |
| 173 | with_content[6] <- "<p>Visible intro content</p>" |
| 174 | expect_identical(merge_title_slide_notes(with_content), with_content) |
| 175 | |
| 176 | # no title slide: nothing to merge |
| 177 | no_title <- lines[-1] |
| 178 | expect_identical(merge_title_slide_notes(no_title), no_title) |
| 179 | }) |
| 180 | |
| 181 | test_that("rendered title slides carry notes and data-timing", { |
| 182 | skip_if_not_pandoc() |
| 183 | skip_if_not_installed("xml2") |
| 184 | rmd <- local_temp_rmd_file( |
| 185 | "---", |
| 186 | "title: Timing test", |
| 187 | "output: revealjs.ids::revealjs_presentation", |
| 188 | "---", |
| 189 | "", |
| 190 | "::: notes", |
| 191 | "Welcome note", |
| 192 | ":::", |
| 193 | "", |
| 194 | "<!-- 00:30 -->", |
| 195 | "", |
| 196 | "## Slide A", |
| 197 | "", |
| 198 | "Content A" |
| 199 | ) |
| 200 | html <- .render_and_read(rmd) |
| 201 | title <- xml2::xml_find_first(html, "//section[@id='title-slide']") |
| 202 | expect_equal(xml2::xml_attr(title, "data-timing"), "30") |
| 203 | expect_equal( |
| 204 | xml2::xml_text(xml2::xml_find_first(title, "./aside[@class='notes']"), trim = TRUE), |
| 205 | "Welcome note" |
| 206 | ) |
| 207 | # no phantom slide between title and first content slide |
| 208 | sections <- xml2::xml_find_all(html, "//section[@class]") |
| 209 | expect_equal( |
| 210 | xml2::xml_attr(sections[1:2], "id"), |
| 211 | c("title-slide", "slide-a") |
| 212 | ) |
| 213 | }) |
| 214 | |
| Marc Kupietz | b066e74 | 2026-08-23 09:57:06 +0300 | [diff] [blame] | 215 | test_that("a user-supplied totalTime is respected", { |
| 216 | skip_if_not_pandoc() |
| 217 | rmd <- local_temp_rmd_file( |
| 218 | "---", |
| 219 | "title: Timing test", |
| 220 | "output: revealjs.ids::revealjs_presentation", |
| 221 | "---", |
| 222 | "", |
| 223 | "## Slide A", |
| 224 | "", |
| 225 | "<!-- 00:30 -->" |
| 226 | ) |
| 227 | html <- .render_and_read(rmd, output_options = list( |
| 228 | reveal_options = list(totalTime = 1800) |
| 229 | )) |
| 230 | # passed through to the config... |
| 231 | expect_true(any(grepl("totalTime:\\s*1800", html))) |
| 232 | # ... and not overwritten by the calculated total |
| 233 | expect_false(any(grepl("totalTime:\\s*30", html))) |
| 234 | }) |
| 235 | |
| 236 | test_that("slide_timing reports totals", { |
| 237 | rmd_file <- tempfile(fileext = ".Rmd") |
| 238 | writeLines(c( |
| 239 | "---", |
| 240 | "title: x", |
| 241 | "output: revealjs.ids::revealjs_presentation", |
| 242 | "---", |
| 243 | "", |
| 244 | "## Slide one", |
| 245 | "", |
| 246 | "<!-- MK 00:30 -->", |
| 247 | "", |
| 248 | "## Slide two", |
| 249 | "", |
| 250 | "<!-- AB 1:00 -->", |
| 251 | "<!-- MK 90 -->" |
| 252 | ), rmd_file) |
| 253 | expect_output(result <- slide_timing(rmd_file), "total") |
| 254 | expect_equal(result$total, 180) |
| 255 | expect_equal(result$speakers$seconds[result$speakers$speaker == "MK"], 120) |
| 256 | expect_equal(result$speakers$seconds[result$speakers$speaker == "AB"], 60) |
| 257 | unlink(rmd_file) |
| 258 | }) |