From 0a7c039dc700f5763725ef92f6caec5f851af70d Mon Sep 17 00:00:00 2001 From: "Joshua Uhalt, Ph.D." Date: Sun, 27 Sep 2026 08:41:32 -0400 Subject: [PATCH 1/2] Release contentvalidR 0.9.0 Stamps the version, release notes, citation metadata, README, and roadmap for the ninth public release, which is about how the package presents itself before 1.0: every reference list in APA 7 form, shorter output that says what it means, component functions and report tables in APA style, figures that show the criterion and the uncertainty, and a README that is a front page. No computed value changes: 25 analyses across every workflow return the same values as v0.8.0. The NEWS intro lists the wording that changes, the two breaking changes (content_report()'s APA default, with format = "data.frame" restoring the numeric table, and the class the component results gain), and the deprecation of agreement_summary(), removed in 1.0.0. Published on GitHub and R-universe. 0.4.0 is still in CRAN's review queue, and CRAN policy asks that no further version be submitted while one is pending. Co-Authored-By: Claude Opus 5.5 --- CITATION.cff | 4 ++-- DESCRIPTION | 2 +- NEWS.md | 49 ++++++++++++++++++++++++++++++++++++++++++------- README.Rmd | 2 +- README.md | 2 +- ROADMAP.md | 29 +++++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 12 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index be80c97..c3b4d02 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,8 +5,8 @@ type: software authors: - family-names: Uhalt given-names: Joshua -version: 0.8.0 -date-released: 2026-09-26 +version: 0.9.0 +date-released: 2026-09-27 repository-code: "https://github.com/JUhalt/contentvalidR" url: "https://juhalt.github.io/contentvalidR/" license: GPL-3.0-only diff --git a/DESCRIPTION b/DESCRIPTION index 81657d4..d7a5f6d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: contentvalidR Type: Package Title: Tools for Substantive and Content Validity Pretesting -Version: 0.8.0.9000 +Version: 0.9.0 Authors@R: c( person(given = "Joshua", family = "Uhalt", email = "Josh.Uhalt@gmail.com", role = c("aut", "cre")) diff --git a/NEWS.md b/NEWS.md index ddf82ba..0146282 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,41 @@ -# contentvalidR 0.8.0.9000 (development version) +# contentvalidR 0.9.0 -## A README that is a front page (in development) +Ninth public release. v0.9.0 is about how the package presents itself before +1.0. Every reference list is in APA 7 form; printed output is shorter and says +what it means; the component functions and report tables print in APA style; +the figures show the criterion and the uncertainty; and the README is a front +page rather than a manual. + +The package continues to declare `Imports: stats` only. + +**No computed value changes.** Across 25 analyses spanning every workflow, +every value 0.8.0 returns is identical in 0.9.0. Some wording changes: the +scale-level summaries of the item-sort and construct-rating workflows name the +Colquitt band of the weaker index, counts read "1 item" or "3 items", and the +note when judge severity cannot be estimated no longer blames near-total +agreement when one dissenting judge causes it. + +**Breaking changes.** + +* `content_report()` writes an APA table by default. For the numeric table + that used to be the default, pass `format = "data.frame"`; its *p* values + now keep three decimals whatever `digits` is. +* The component functions' results (`compute_psa()`, `aikens_v()`, `cvr()`, + and eleven others) gain a class, so they print as formatted tables. Every + value is unchanged and `as.data.frame()` returns the plain data frame; only + code that tests `class(x) == "data.frame"` exactly would notice. + +**Deprecated.** `agreement_summary()` warns and will be removed in 1.0.0; use +`panel_agreement()`, which takes raters in rows like every other ratings +function. + +New: `content_report(format = "apa")`, and `csv_binom_test()` returns its +inputs (`n_target`, `N`, `p0`, `alpha`) beside its result. + +Published on GitHub and R-universe. 0.4.0 is still in CRAN's review queue, and +CRAN policy asks that no further version be submitted while one is pending. + +## A README that is a front page * **The README is a front page rather than a manual.** It ran to about 1,400 lines of printed output: every workflow in full, the component functions, @@ -15,7 +50,7 @@ design would need. Its method list now names all six workflows, and it has a reference list. -## Plots show the criterion and the uncertainty (in development) +## Plots show the criterion and the uncertainty No computed value changes; this is about what the figures draw. @@ -41,7 +76,7 @@ No computed value changes; this is about what the figures draw. * **Every figure in the README and the vignettes has alt text** describing what it shows; 17 of 19 had none. -## Shorter keys, formatted components, and APA report tables (in development) +## Shorter keys, formatted components, and APA report tables No computed value changes; this is about what is printed. @@ -73,7 +108,7 @@ No computed value changes; this is about what is printed. `digits` is. The construct-rating table shows the contrast *p* beside the omnibus *p*, since a Review item can have an omnibus *p* below .001. -## `agreement_summary()` is deprecated (in development) +## `agreement_summary()` is deprecated * **Deprecated in 0.9.0, to be removed in 1.0.0.** Use `panel_agreement()`. `agreement_summary()` is the only function that takes items in rows; every @@ -84,7 +119,7 @@ No computed value changes; this is about what is printed. it at 1.0.0 leaves nothing deprecated past 1.0, as the stability policy in `?contentvalidR` sets out. -## References follow APA 7 everywhere (in development) +## References follow APA 7 everywhere * **One reference list, installed in BibTeX form.** `REFERENCES.bib` held 13 of the works the package cites, and one it no longer cited; it now holds all @@ -104,7 +139,7 @@ No computed value changes; this is about what is printed. * `citation("contentvalidR")` prints the package reference in APA form, with its version and "[Computer software]". -## Printed output says what it means (in development) +## Printed output says what it means * **The expert-panel print opens with its verdict**, as the other workflows' prints do: how many items met the criterion, then the flagged items by name. diff --git a/README.Rmd b/README.Rmd index a464af2..20b9e14 100644 --- a/README.Rmd +++ b/README.Rmd @@ -25,7 +25,7 @@ if (!requireNamespace("contentvalidR", quietly = TRUE)) { [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html) -**Current stable release: 0.8.0.** +**Current stable release: 0.9.0.** Development plans: [Roadmap](https://github.com/JUhalt/contentvalidR/blob/master/ROADMAP.md) · [Future research](https://github.com/JUhalt/contentvalidR/milestone/3) · [Open issues](https://github.com/JUhalt/contentvalidR/issues). Documentation: [Package website](https://juhalt.github.io/contentvalidR/) · [Changelog](https://juhalt.github.io/contentvalidR/news/index.html). diff --git a/README.md b/README.md index aa3fb8e..a5ed1a6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ only](https://img.shields.io/badge/License-GPL--3.0--only-blue.svg)](https://git experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html) -**Current stable release: 0.8.0.** +**Current stable release: 0.9.0.** Development plans: [Roadmap](https://github.com/JUhalt/contentvalidR/blob/master/ROADMAP.md) diff --git a/ROADMAP.md b/ROADMAP.md index f673bab..15c93d2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -747,6 +747,35 @@ tag. *** +## v0.9.0 - Ready to be read: references, output, plots, and the README + +**Status:** Released (0.9.0 on 2026-09-27, on GitHub and R-universe) +**Version:** `0.9.0` + +Work: [#72](https://github.com/JUhalt/contentvalidR/pull/72) to +[#75](https://github.com/JUhalt/contentvalidR/pull/75). The maintainer asked +for everything a user sees to be ready before 1.0; an output gallery of every +print, summary, and plot set the list. + +- [x] Put every reference list in APA 7 form and fix what the output misstated + ([#72](https://github.com/JUhalt/contentvalidR/pull/72)): the installed + BibTeX file holds all 53 cited works, checked against Crossref; lists are + alphabetical with en dashes; the expert-panel print opens with its + verdict; the modified-kappa range is corrected. +- [x] Show a short key tailored to each result, print the component functions + in APA style, make `content_report()` APA by default, and deprecate + `agreement_summary()` ([#73](https://github.com/JUhalt/contentvalidR/pull/73)). +- [x] Make the plots show the criterion and the uncertainty, and give every + figure alt text ([#74](https://github.com/JUhalt/contentvalidR/pull/74)). +- [x] Make the README a front page and move its planning examples to a + vignette ([#75](https://github.com/JUhalt/contentvalidR/pull/75)). + +CRAN: unchanged. 0.4.0 is still in review, so 0.9.0 is not submitted; whichever +version is current when 0.4.0 resolves goes to CRAN as the update, from its own +tag. + +*** + ## v1.0.0 - Joint stable release with nomologR (proposed) **Status:** Proposed by the maintainer on 2026-09-19. Not scheduled. From 3f724fa3b7722cce773aa30421970e2acedd0d1f Mon Sep 17 00:00:00 2001 From: "Joshua Uhalt, Ph.D." Date: Sun, 27 Sep 2026 09:33:30 -0400 Subject: [PATCH 2/2] Test every component print, the APA reports, and the legacy paths The 0.9.0 print methods were only half covered: R/component_print.R stood at 57% in CI, which pulled overall coverage from 94.5% (0.8.0) to 92.9%. Tests only; no package code changes. - test-component-print.R: every one of the 14 component prints, with the branches their notes depend on: no interval, missing assignments, a bootstrap interval, a between-judges ANOVA (no Greenhouse-Geisser note), expert judges (no Colquitt band), a chi-square that cannot be computed, pairs sorted by different numbers of judges, a csv_binom_test result saved before 0.9.0, and the plain print after subsetting. - test-reporting.R: the APA report for every workflow and expert mode (construct rating, essentiality, congruence, Delphi, domain), and the note an empty APA report prints. - test-legacy-paths.R (new): the Monte Carlo helpers' argument checks and results, older decision wording mapping onto the shared statuses, the workflow constructor's checks and legacy aliases, and expert objects saved before 0.0.6 in all three modes. Coverage: 95.6% measured locally with covr (92.9% before, 94.5% at 0.8.0). All 2,940 tests pass. Co-Authored-By: Claude Opus 5.5 --- tests/testthat/test-component-print.R | 121 ++++++++++++++++++++++++++ tests/testthat/test-legacy-paths.R | 106 ++++++++++++++++++++++ tests/testthat/test-reporting.R | 53 +++++++++++ 3 files changed, 280 insertions(+) create mode 100644 tests/testthat/test-legacy-paths.R diff --git a/tests/testthat/test-component-print.R b/tests/testthat/test-component-print.R index 78073cc..6c92d65 100644 --- a/tests/testthat/test-component-print.R +++ b/tests/testthat/test-component-print.R @@ -79,3 +79,124 @@ test_that("a similarity matrix prints formatted and still feeds content_structur expect_match(shown(sim), "Every pair was sorted by the same 5 judges.", fixed = TRUE) expect_s3_class(content_structure(sim), "contentvalid_structure") }) + +test_that("pairs sorted by different numbers of judges are reported as a range", { + d <- data.frame(item = rep(paste0("I", 1:4), each = 5), rater = rep(1:5, 4), + assigned_construct = c(rep("A", 10), rep("B", 10)), + stringsAsFactors = FALSE) + d <- d[!(d$item == "I4" & d$rater == 5), ] + expect_match(shown(similarity_from_sort(d)), + "Pairs were sorted by 4 to 5 judges", fixed = TRUE) +}) + +ratings_within <- function() { + set.seed(3) + d <- expand.grid(item = c("A1", "B1"), rater = 1:12, + construct = c("A", "B", "C"), stringsAsFactors = FALSE) + d$target_construct <- ifelse(d$item == "B1", "B", "A") + d$rating <- ifelse(d$construct == d$target_construct, + sample(4:5, nrow(d), TRUE), sample(1:3, nrow(d), TRUE)) + d +} + +test_that("the construct-rating components print as APA tables", { + d <- ratings_within() + out_htc <- shown(htc(d, scale_min = 1, scale_max = 5)) + expect_match(out_htc, "Hinkin-Tracey correspondence (HTC", fixed = TRUE) + expect_match(out_htc, "share of the 5-point scale", fixed = TRUE) + expect_false(grepl(" 0[.][0-9]", out_htc)) + + out_htd <- shown(htd(d, scale_min = 1, scale_max = 5)) + expect_match(out_htd, "Hinkin-Tracey distinctiveness (HTD", fixed = TRUE) + expect_match(out_htd, "competitor mean", fixed = TRUE) + expect_match(out_htd, "averages the gap over every other construct", fixed = TRUE) + + within <- shown(anova_content(d)) + expect_match(within, "Greenhouse-Geisser corrected", fixed = TRUE) + expect_match(within, "F[(][0-9.]+, [0-9.]+[)] = [0-9.]+") + # A between-judges design has whole-number degrees of freedom and no + # Greenhouse-Geisser note. + between <- d + between$rater <- paste0(between$rater, "-", between$construct) + out_between <- shown(anova_content(between, design = "between")) + expect_match(out_between, "Content-validity ANOVA", fixed = TRUE) + expect_false(grepl("Greenhouse-Geisser corrected", out_between, fixed = TRUE)) +}) + +test_that("the item-sort and expert components print their notes", { + csv <- shown(compute_csv(sorts)) + expect_match(csv, "Coefficient of substantive validity (Csv", fixed = TRUE) + expect_match(csv, "18/20 B 2/20 .80", fixed = TRUE) + expect_match(csv, "divided by the number of judges", fixed = TRUE) + + # Without an interval there is no interval column or interval note. + no_ci <- shown(compute_psa(sorts, ci = "none")) + expect_false(grepl("95% CI", no_ci, fixed = TRUE)) + # Missing assignments are counted in a note. + gappy <- sorts + gappy$assigned_construct[1] <- NA + expect_match(shown(compute_psa(gappy)), "Missing assignments: 1", fixed = TRUE) + + R <- matrix(c(4, 4, 3, 4, 3, 4, 4, 4, 4), 3, + dimnames = list(NULL, paste0("I", 1:3))) + expect_match(shown(aikens_v(R, lo = 1, hi = 4)), + "Penfield-Giacobbi score (Penfield & Giacobbi, 2004)", fixed = TRUE) + expect_false(grepl("Interval:", shown(aikens_v(R, lo = 1, hi = 4, ci = "none")), + fixed = TRUE)) + expect_match(shown(aikens_v(R, lo = 1, hi = 4, ci = "bootstrap", B = 50, + seed = 1)), + "Interval: percentile bootstrap.", fixed = TRUE) + + d <- expand.grid(item = c("I1", "I2"), judge = 1:4, objective = c("A", "B")) + d$score <- ifelse((d$item == "I1") == (d$objective == "A"), 1, -1) + out_ioc <- shown(ioc(d)) + expect_match(out_ioc, "Item-objective congruence (IOC", fixed = TRUE) + expect_match(out_ioc, "I1 A 4 1.00", fixed = TRUE) +}) + +test_that("the Colquitt functions print their bands", { + bands <- shown(interpret_colquitt(c(.70, .40), "csv")) + expect_match(bands, "Benchmark bands (Colquitt et al., 2019)", fixed = TRUE) + expect_match(bands, "Csv .70 Strong", fixed = TRUE) + expect_match(bands, "not a universal cutoff", fixed = TRUE) + # Expert judges get no band. + expect_match(shown(interpret_colquitt(.70, "csv", judge_type = "expert")), + "not applied", fixed = TRUE) + + norms <- shown(colquitt_benchmarks("htd")) + expect_match(norms, "Benchmarks for HTD (Colquitt et al., 2019)", fixed = TRUE) + expect_match(norms, "Lack of 0th-19th none", fixed = TRUE) +}) + +test_that("the two-by-two helpers print their statistics", { + pred <- c(TRUE, TRUE, FALSE, FALSE, TRUE, FALSE) + act <- c(TRUE, FALSE, FALSE, FALSE, TRUE, TRUE) + sig <- shown(signal_detection(pred, act)) + expect_match(sig, "Retention decisions compared with the actual outcome", + fixed = TRUE) + expect_match(sig, "accuracy = ", fixed = TRUE) + expect_match(sig, "chi-square(1) = ", fixed = TRUE) + rep_out <- shown(reproducibility_phi(pred, act)) + expect_match(rep_out, "Retention decisions in two pretests", fixed = TRUE) + expect_match(rep_out, "phi = ", fixed = TRUE) + # When chi-square cannot be computed, only phi is reported. + flat <- shown(reproducibility_phi(rep(TRUE, 4), rep(TRUE, 4))) + expect_false(grepl("chi-square", flat, fixed = TRUE)) +}) + +test_that("a csv_binom_test result saved before 0.9.0 still prints", { + b <- unclass(csv_binom_test(15, 20)) + b[c("n_target", "N", "p0", "alpha")] <- NULL + class(b) <- c("contentvalid_binom", "contentvalid_component") + expect_match(shown(b), "Psa = .75, p = .021. Decision: significant.", + fixed = TRUE) +}) + +test_that("each component print falls back to a plain print after subsetting", { + d <- ratings_within() + expect_match(shown(htd(d, scale_min = 1, scale_max = 5)[, c("item", "htd")]), + "item", fixed = TRUE) + expect_false(grepl("Hinkin-Tracey", shown(htc(d, scale_min = 1, + scale_max = 5)[, c("item", "htc")]), + fixed = TRUE)) +}) diff --git a/tests/testthat/test-legacy-paths.R b/tests/testthat/test-legacy-paths.R new file mode 100644 index 0000000..13fd0c5 --- /dev/null +++ b/tests/testthat/test-legacy-paths.R @@ -0,0 +1,106 @@ +# Paths the everyday tests do not reach: the Monte Carlo helpers kept for +# older analyses, and the compatibility code that lets objects saved by +# earlier versions still print and summarize. + +test_that("the simulation helpers check their arguments", { + expect_error(simulate_csv_power(N = 0), "`N`") + expect_error(simulate_csv_power(N = 2.5), "`N`") + expect_error(simulate_csv_power(true_p = 1.5), "`true_p`") + expect_error(simulate_csv_power(reps = 0), "`reps`") + expect_error(simulate_csv_power(alpha = 1), "`alpha`") + + expect_error(simulate_anova_power(n_raters = 1), "`n_raters`") + expect_error(simulate_anova_power(mean_diff = NA), "`mean_diff`") + expect_error(simulate_anova_power(sd = 0), "`sd`") + expect_error(simulate_anova_power(k_constructs = 1), "`k_constructs`") + expect_error(simulate_anova_power(reps = 0), "`reps`") + expect_error(simulate_anova_power(alpha = 0), "`alpha`") +}) + +test_that("the simulation helpers return a power between 0 and 1", { + set.seed(8) + p_csv <- simulate_csv_power(N = 20, true_p = 0.9, reps = 60) + expect_true(p_csv >= 0 && p_csv <= 1) + # With 20 judges choosing the target nine times in ten, the exact test + # nearly always passes. + expect_gt(p_csv, 0.8) + set.seed(8) + p_anova <- simulate_anova_power(n_raters = 10, mean_diff = 2, sd = 1, + k_constructs = 3, reps = 20) + expect_true(p_anova >= 0 && p_anova <= 1) + expect_gt(p_anova, 0.8) +}) + +test_that("older decision wording still maps onto the shared statuses", { + old <- c("Insufficient judges", "Needs review", "Retained", "Favoured", + "Descriptive summary", NA) + expect_identical( + contentvalidR:::.workflow_status_from_recommendation(old), + c("Insufficient data", "Review", "Supported", "Supported", + "Descriptive only", NA) + ) +}) + +test_that("the workflow constructor rejects malformed parts", { + good <- data.frame(item = "I1", recommendation = "Retain") + build <- function(...) { + args <- list(subclass = "contentvalid_sort", workflow = "item-sort", + results = good, scale_summary = data.frame(), + settings = list(), design = list()) + dots <- list(...) + args[names(dots)] <- dots + do.call(contentvalidR:::.new_contentvalid_workflow, args) + } + expect_s3_class(build(), "contentvalid_workflow") + expect_error(build(results = "x"), "must be a data.frame") + expect_error(build(results = data.frame(item = "I1")), "`recommendation` or `status`") + expect_error(build(results = data.frame(item = "I1", status = "Maybe")), + "must be one of") + expect_error(build(scale_summary = "x"), "`scale_summary` must be a data.frame") + expect_error(build(settings = "x"), "must be lists") + expect_error(build(details = "x"), "`details` must be a list") + # Legacy aliases are added without overwriting a current part. + fit <- build(legacy = list(scale = data.frame(a = 1), results = "ignored")) + expect_identical(fit$scale, data.frame(a = 1)) + expect_s3_class(fit$results, "data.frame") +}) + +test_that("expert objects saved before 0.0.6 still summarize", { + rel <- structure(list( + mode = "relevance", + results = data.frame(item = c("I1", "I2"), + recommendation = c("Strong support", "Review")), + scale = data.frame(n_items = 2L, n_experts_min = 5L, n_experts_max = 6L) + ), class = c("contentvalid_expert", "contentvalid_workflow")) + expect_identical(contentvalidR:::.workflow_name(rel), "expert-panel") + d <- contentvalidR:::.workflow_design(rel) + expect_identical(d$type, "expert-panel relevance") + expect_identical(d$n_judges_max, 6L) + s <- contentvalidR:::.workflow_summary_core(rel) + expect_identical(c(s$n_supported, s$n_review), c(1L, 1L)) + + ess <- structure(list( + mode = "essentiality", + results = data.frame(item = c("I1", "I2"), N = c(10L, 10L), + recommendation = c("Supported", "Review")) + ), class = c("contentvalid_expert", "contentvalid_workflow")) + expect_identical(contentvalidR:::.workflow_design(ess)$n_judges, 10L) + + con <- structure(list( + mode = "congruence", + results = data.frame(item = c("I1", "I1"), recommendation = "Descriptive only"), + details = list(cells = data.frame(objective = c("A", "B"), + n_judges = c(4L, 3L), n_missing = c(0L, 1L))) + ), class = c("contentvalid_expert", "contentvalid_workflow")) + cd <- contentvalidR:::.workflow_design(con) + expect_identical(cd$n_items, 1L) + expect_identical(c(cd$n_judges_min, cd$n_judges_max, cd$n_missing), c(3L, 4L, 1L)) + expect_identical(cd$n_objectives, 2L) + + # Anything else unknown reports no design and no workflow name. + other <- structure(list(results = data.frame(recommendation = "Retain")), + class = "contentvalid_workflow") + expect_identical(contentvalidR:::.workflow_design(other), list()) + expect_identical(contentvalidR:::.workflow_name(other), NA_character_) + expect_identical(contentvalidR:::.workflow_scale_summary(other), data.frame()) +}) diff --git a/tests/testthat/test-reporting.R b/tests/testthat/test-reporting.R index 794e1e2..40697a0 100644 --- a/tests/testthat/test-reporting.R +++ b/tests/testthat/test-reporting.R @@ -210,3 +210,56 @@ test_that("markdown output prints as the table, not as a character vector", { expect_false(any(grepl("^[[]1[]]|settings", out))) expect_match(out[1], "| Psa | 95% CI |", fixed = TRUE) }) + +test_that("every workflow and expert mode has an APA report with a decision", { + set.seed(2) + rd <- expand.grid(item = c("A1", "B1"), rater = 1:12, + construct = c("A", "B", "C"), stringsAsFactors = FALSE) + rd$target_construct <- ifelse(rd$item == "B1", "B", "A") + rd$rating <- ifelse(rd$construct == rd$target_construct, + sample(4:5, nrow(rd), TRUE), sample(1:3, nrow(rd), TRUE)) + rating <- content_report(rating_validity(rd)) + expect_identical(names(rating), c("item", "target", "judges", "competitor", + "HTC", "HTD", "omnibus p", "contrast p", + "decision")) + + ess <- content_report(expert_validity(c(10, 8), mode = "essentiality", N = 12)) + expect_identical(names(ess), c("item", "essential", "CVR", "p", "decision")) + expect_identical(ess$essential, c("10/12", "8/12")) + + d <- expand.grid(item = c("I1", "I2"), judge = 1:4, objective = c("A", "B")) + d$target_objective <- ifelse(d$item == "I1", "A", "B") + d$score <- ifelse(d$objective == d$target_objective, 1, -1) + con <- content_report(expert_validity(d, mode = "congruence")) + expect_identical(names(con), c("item", "target", "target IOC", "competitor", + "competitor IOC", "margin", "decision")) + expect_identical(con$margin, c("2.00", "2.00")) + + long <- function(m, round) { + data.frame(expert = paste0("E", seq_len(nrow(m))), + item = rep(colnames(m), each = nrow(m)), round = round, + rating = as.vector(m), stringsAsFactors = FALSE) + } + r1 <- cbind(S1 = c(4, 4, 3, 4, 3, 4), S2 = c(3, 4, 3, 2, 4, 3)) + r2 <- cbind(S1 = c(4, 4, 4, 4, 3, 4), S2 = c(4, 4, 4, 4, 4, 3)) + delphi <- content_report(delphi_validity(rbind(long(r1, 1), long(r2, 2)), + lo = 1, hi = 4, B = 0)) + expect_true(all(c("item", "last round", "experts", "agree", "unchanged", + "decision") %in% names(delphi))) + + dom <- content_report(domain_validity( + data.frame(item = paste0("I", 1:3), cell = c("A", "A", "B"), + stringsAsFactors = FALSE), + domain = c("A", "B", "C"))) + expect_identical(dom$share, c("67%", "33%", "0%")) +}) + +test_that("an APA report with no rows prints a note rather than an empty table", { + clean <- sort_validity(data.frame( + item = rep("I1", 6), rater = 1:6, + assigned_construct = rep("A", 6), target_construct = "A", + stringsAsFactors = FALSE + )) + out <- utils::capture.output(print(content_report(clean, include = "flagged"))) + expect_identical(out, "No units matched the requested selection.") +})