tests/regression-tests.R

# Regression tests for semPlot (plain stopifnot-style, no test framework).
# Structure: baseline pipeline tests, then per-fix regression blocks.
# Long-running blocks (psychonetrics model fitting) are skipped on CRAN
# (set NOT_CRAN=true to run them).
library(semPlot)
library(lavaan)
options(warn = 1)

ok <- 0L; fail <- 0L; fails <- character(0)
check <- function(label, expr){
  res <- tryCatch(isTRUE(expr), error = function(e) structure(FALSE, msg = conditionMessage(e)))
  if (isTRUE(res)) { ok <<- ok + 1L; cat("PASS:", label, "\n") }
  else { fail <<- fail + 1L; fails <<- c(fails, label)
         cat("FAIL:", label, if (!is.null(attr(res,"msg"))) paste0(" [", attr(res,"msg"), "]") else "", "\n") }
}
quiet <- function(expr) suppressWarnings(suppressMessages(expr))
not_cran <- identical(tolower(Sys.getenv("NOT_CRAN")), "true")

HS <- HolzingerSwineford1939

## ================= BASELINE =================
fit_cfa <- quiet(cfa(" visual =~ x1 + x2 + x3\n textual =~ x4 + x5 + x6 ", data = HS))
check("B1 lavaan CFA imports", { m <- quiet(semPlotModel(fit_cfa)); nrow(m@Pars) > 10 })
check("B2 lavaan CFA renders", { p <- quiet(semPaths(fit_cfa, DoNotPlot = TRUE)); inherits(p, "qgraph") })

fit_mg <- quiet(cfa(" visual =~ x1 + x2 + x3 ", data = HS, group = "school"))
check("B3 multigroup renders (list of qgraph)", {
  p <- quiet(semPaths(fit_mg, DoNotPlot = TRUE)); is.list(p) && all(sapply(p, inherits, "qgraph")) })

check("B4 semSyntax roundtrip", {
  syn <- quiet(capture.output(s <- semSyntax(fit_cfa, "lavaan")))
  fit2 <- quiet(cfa(s, data = HS))
  inherits(fit2, "lavaan") && length(coef(fit2)) == length(coef(fit_cfa)) })

d_lm <- local({ set.seed(1); d <- data.frame(x = rnorm(200), z = rnorm(200)); d$y <- 0.5*d$x - 0.7*d$z + rnorm(200); d })
fit_lm <- lm(y ~ x + z, data = d_lm)
check("B5 lm imports", { m <- quiet(semPlotModel(fit_lm)); sum(m@Pars$edge == "->") == 2 })

fit_sam <- quiet(sam(" visual =~ x1+x2+x3\n textual =~ x4+x5+x6\n visual ~ textual ", data = HS))
check("B6 sam() renders", { p <- quiet(semPaths(fit_sam, DoNotPlot = TRUE)); inherits(p, "qgraph") || is.list(p) })

check("B7 lavaan model string imports", {
  m <- quiet(semPlotModel(" visual =~ x1 + x2 + x3 "))
  inherits(m, "semPlotModel") })



## ================= item01 =================

# Item 1: AST-based `+` handling (formerly deparse-split crash)
d1 <- local({ set.seed(2); d <- data.frame(x = rnorm(100), z = rnorm(100)); d$y <- d$x - d$z + rnorm(100); d })

check("T1a inline lm with + in formula renders", {
  p <- quiet(semPaths(lm(y ~ x + z, data = d1), DoNotPlot = TRUE)); inherits(p, "qgraph") })

check("T1b inline cfa call with + in model string renders", {
  p <- quiet(semPaths(cfa("visual =~ x1 + x2 + x3", data = HS), DoNotPlot = TRUE)); inherits(p, "qgraph") })

check("T1c index arithmetic in argument works", {
  mods <- list(fit_cfa, fit_cfa)
  m <- quiet(semPlotModel(mods[[1 + 1]])); inherits(m, "semPlotModel") })

check("T1d combining two lavaan fits with + still works", {
  fitA <- quiet(cfa(" visual =~ x1 + x2 + x3 ", data = HS))
  fitB <- quiet(cfa(" textual =~ x4 + x5 + x6 ", data = HS))
  m <- quiet(semPlotModel(fitA + fitB))
  inherits(m, "semPlotModel") && length(m@Original) == 2 &&
    all(c("visual","textual") %in% m@Vars$name) })

check("T1e combining via semPaths renders", {
  fitA <- quiet(cfa(" visual =~ x1 + x2 + x3 ", data = HS))
  fitB <- quiet(cfa(" textual =~ x4 + x5 + x6 ", data = HS))
  p <- quiet(semPaths(fitA + fitB, DoNotPlot = TRUE))
  inherits(p, "qgraph") || (is.list(p) && all(sapply(p, inherits, "qgraph"))) })

check("T1f combining two semPlotModel objects still works", {
  mA <- quiet(semPlotModel(cfa(" visual =~ x1 + x2 + x3 ", data = HS)))
  mB <- quiet(semPlotModel(cfa(" textual =~ x4 + x5 + x6 ", data = HS)))
  m <- quiet(semPlotModel(mA + mB)); inherits(m, "semPlotModel") && length(m@Original) == 2 })

check("T1g non-model input still errors informatively", {
  e <- tryCatch({ semPlotModel(1 + 1); NULL }, error = function(e) conditionMessage(e))
  !is.null(e) && grepl("not recognized", e) })


## ================= item02 =================

# Item 2: GLM std fallback must yield raw coefficients, not NA
d2 <- local({ set.seed(3); d <- data.frame(x = rnorm(300), z = rnorm(300));
              d$y <- rbinom(300, 1, plogis(0.5*d$x - 0.7*d$z)); d })

check("T2a binomial glm std column has non-NA slopes", {
  fitg <- glm(y ~ x + z, data = d2, family = binomial)
  m <- quiet(semPlotModel(fitg))
  slopes <- m@Pars$std[m@Pars$edge == "->"]
  sum(!is.na(slopes)) == 2 })

check("T2b binomial glm std equals raw coefficients (fallback)", {
  fitg <- glm(y ~ x + z, data = d2, family = binomial)
  m <- quiet(semPlotModel(fitg))
  co <- coef(fitg)
  sl <- m@Pars[m@Pars$edge == "->", ]
  all(abs(sl$std[match(c("x","z"), sl$lhs)] - co[c("x","z")]) < 1e-10) })

check("T2c binomial glm intercept std stays NA", {
  fitg <- glm(y ~ x + z, data = d2, family = binomial)
  m <- quiet(semPlotModel(fitg))
  is.na(m@Pars$std[m@Pars$edge == "int"]) })

if (requireNamespace("rockchalk", quietly = TRUE)) check("T2d gaussian lm std still matches rockchalk", {
  fitl <- lm(y ~ x + z, data = d2)
  m <- quiet(semPlotModel(fitl))
  sc <- coef(rockchalk::standardize(fitl))
  sl <- m@Pars[m@Pars$edge == "->", ]
  all(abs(sl$std[match(c("x","z"), sl$lhs)] - sc[c("xs","zs")]) < 1e-10) })

check("T2e binomial glm renders with what='std'", {
  fitg <- glm(y ~ x + z, data = d2, family = binomial)
  p <- quiet(semPaths(fitg, what = "std", DoNotPlot = TRUE))
  inherits(p, "qgraph") })


## ================= item03 =================

# Item 3: graphics-state hygiene.
# T3a is a strong whole-suite assertion: every earlier test used DoNotPlot=TRUE,
# so if the fixes hold, NO graphics device may exist at this point.
check("T3a no graphics device opened by any DoNotPlot call so far", is.null(dev.list()))

check("T3b par(ask) restored after an error mid-plot", {
  pdf(NULL); on.exit(dev.off(), add = TRUE)
  par(ask = FALSE)
  # force an error inside the plot loop AFTER par(ask=TRUE) is set:
  try(suppressWarnings(semPaths(fit_cfa, ask = TRUE,
      edge.color = function(x) x)), silent = TRUE)
  isTRUE(par("ask") == FALSE) })

check("T3c layout restored after semCors", {
  pdf(NULL); on.exit(dev.off(), add = TRUE)
  quiet(try(semCors(fit_cfa), silent = TRUE))
  # after semCors exits, a new plot must occupy the full device again:
  plot.new(); pr <- par("mfg")
  pr[3] == 1 && pr[4] == 1 })


## ================= item04 =================

# Item 4: informative errors from the importer cascade
check("T4a garbage string error names the lavaan importer failure", {
  e <- tryCatch({ semPlotModel("definitely ~~~ not // a model"); NULL },
                error = function(e) conditionMessage(e))
  !is.null(e) && grepl("lavaan", e, ignore.case = TRUE) && !identical(e, "Object not recognized as SEM model") })

check("T4b unrecognized FILE reports per-importer failures", {
  f <- tempfile(fileext = ".txt")
  writeLines(c("this is", "just some text"), f)
  e <- tryCatch({ semPlotModel(f); NULL }, error = function(e) conditionMessage(e))
  unlink(f)
  !is.null(e) && grepl("Importer attempts failed", e) && grepl("Mplus", e) })

check("T4c valid lavaan model string still imports", {
  m <- quiet(semPlotModel(" visual =~ x1 + x2 + x3 "))
  inherits(m, "semPlotModel") })


## ================= item06 =================

# Item 6: polish bundle
check("T6a predictor named intercept_time stays a path", {
  d <- local({ set.seed(4); data.frame(intercept_time = rnorm(50), y = rnorm(50)) })
  m <- quiet(semPlotModel(lm(y ~ intercept_time, data = d)))
  sum(m@Pars$edge == "->" & m@Pars$lhs == "intercept_time") == 1 &&
    sum(m@Pars$edge == "int") == 1 })

check("T6b semCors errors informatively on lm models", {
  d <- data.frame(x = rnorm(30), y = rnorm(30))
  e <- tryCatch({ semCors(quiet(semPlotModel(lm(y ~ x, data = d)))); NULL },
                error = function(e) conditionMessage(e))
  !is.null(e) && grepl("covariance", e) })

check("T6c semSyntax warns and skips interaction terms from lm", {
  d <- local({ set.seed(5); d <- data.frame(x = rnorm(80), z = rnorm(80)); d$y <- d$x * d$z + rnorm(80); d })
  m <- quiet(semPlotModel(lm(y ~ x * z, data = d)))
  w <- NULL
  s <- withCallingHandlers(
    capture.output(res <- semSyntax(m, "lavaan")),
    warning = function(cond){ w <<- c(w, conditionMessage(cond)); invokeRestart("muffleWarning") })
  any(grepl("Interaction", w)) && !grepl("par[0-9]+\\*", res) })

check("T6d main-effect syntax from lm still emitted", {
  d <- data.frame(x = rnorm(30)); d$y <- d$x + rnorm(30)
  m <- quiet(semPlotModel(lm(y ~ x, data = d)))
  s <- quiet(capture.output(res <- semSyntax(m, "lavaan")))
  grepl("y ~", res) })


## ================= item08 =================

# Item 8: lavaan modernization — composites, multilevel, key-based alignment
names(HS)[names(HS) == "x9"] -> .x9nm  # keep HS intact; use a copy
HS8 <- HS

check("T8a composite (<~) edges point INTO the composite", {
  fitc <- quiet(sem(" comp <~ 1*x1 + x2 + x3\n x9 ~ comp ", data = HS8))
  m <- quiet(semPlotModel(fitc))
  ce <- m@Pars[m@Pars$composite %in% TRUE, ]
  nrow(ce) == 3 && all(ce$edge == "~>") && all(ce$rhs == "comp") &&
    all(ce$lhs %in% c("x1","x2","x3")) && "comp" %in% m@Vars$name[!m@Vars$manifest] })

check("T8b composite model renders", {
  fitc <- quiet(sem(" comp <~ 1*x1 + x2 + x3\n x9 ~ comp ", data = HS8))
  inherits(quiet(semPaths(fitc, DoNotPlot = TRUE)), "qgraph") })

check("T8c semSyntax re-emits <~ and refits", {
  fitc <- quiet(sem(" comp <~ 1*x1 + x2 + x3\n x9 ~ comp ", data = HS8))
  s <- quiet(capture.output(res <- semSyntax(fitc, "lavaan")))
  grepl("comp <~", res) &&
    { fit2 <- quiet(sem(res, data = HS8)); inherits(fit2, "lavaan") &&
        length(coef(fit2)) == length(coef(fitc)) } })

check("T8d multilevel: Pars carry Within/Between and levels are separated", {
  fit2 <- quiet(sem(" level: 1\n fw =~ y1 + y2 + y3\n level: 2\n fb =~ y1 + y2 + y3 ",
                    data = Demo.twolevel, cluster = "cluster"))
  m <- quiet(semPlotModel(fit2))
  !is.null(m@Pars$BetweenWithin) &&
    all(sort(unique(m@Pars$BetweenWithin)) == c("Between","Within")) &&
    all(m@Pars$BetweenWithin[m@Pars$lhs == "fw" | m@Pars$rhs == "fw"] == "Within") &&
    all(m@Pars$BetweenWithin[m@Pars$lhs == "fb" | m@Pars$rhs == "fb"] == "Between") })

check("T8e multilevel renders two panels (Within and Between)", {
  fit2 <- quiet(sem(" level: 1\n fw =~ y1 + y2 + y3\n level: 2\n fb =~ y1 + y2 + y3 ",
                    data = Demo.twolevel, cluster = "cluster"))
  p <- quiet(semPaths(fit2, DoNotPlot = TRUE, ask = FALSE))
  is.list(p) && length(p) == 2 && all(sapply(p, inherits, "qgraph")) })

check("T8f multilevel block covariances named Within/Between", {
  fit2 <- quiet(sem(" level: 1\n fw =~ y1 + y2 + y3\n level: 2\n fb =~ y1 + y2 + y3 ",
                    data = Demo.twolevel, cluster = "cluster"))
  m <- quiet(semPlotModel(fit2))
  identical(names(m@ObsCovs), c("Within","Between")) &&
    identical(names(m@ImpCovs), c("Within","Between")) })

check("T8g key-based alignment: defined parameters and labels intact", {
  fitd <- quiet(sem(" visual =~ x1 + a*x2 + b*x3\n ab := a*b ", data = HS8))
  m <- quiet(semPlotModel(fitd))
  # := row removed from Pars; labeled loadings a,b kept with free par ids:
  all(!grepl(":=", m@Pars$edge)) &&
    sum(m@Pars$label %in% c("a","b")) == 2 &&
    all(m@Pars$par[m@Pars$label %in% c("a","b")] > 0) })

check("T8h ordered/threshold model still imports with thresholds", {
  d <- HS8[paste0("x",1:4)]
  for (v in names(d)) d[[v]] <- cut(d[[v]], 3, labels = FALSE, ordered_result = TRUE)
  fito <- quiet(cfa(" f =~ x1 + x2 + x3 + x4 ", data = d, ordered = names(d)))
  m <- quiet(semPlotModel(fito))
  nrow(m@Thresholds) > 0 && inherits(quiet(semPaths(fito, DoNotPlot = TRUE)), "qgraph") })

check("T8i combining lavaan and lm models now works (column union)", {
  d <- data.frame(x = rnorm(40)); d$y <- d$x + rnorm(40)
  mA <- quiet(semPlotModel(cfa(" visual =~ x1 + x2 + x3 ", data = HS8)))
  mB <- quiet(semPlotModel(lm(y ~ x, data = d)))
  m <- mA + mB
  inherits(m, "semPlotModel") && "knot" %in% names(m@Pars) && "composite" %in% names(m@Pars) })


## ================= item09 =================

# Item 9: efaList (lavaan::efa) importer
check("T9a efa() single-solution imports and renders", {
  ef <- quiet(efa(data = HS[paste0("x",1:9)], nfactors = 2))
  m <- quiet(semPlotModel(ef))
  inherits(m, "semPlotModel") && sum(!m@Vars$manifest) == 2 &&
    inherits(quiet(semPaths(ef, DoNotPlot = TRUE)), "qgraph") })

check("T9b efa() multi-solution: default is last, 'which' selects", {
  ef <- quiet(efa(data = HS[paste0("x",1:9)], nfactors = 1:2))
  m_def <- quiet(semPlotModel(ef))
  m_1 <- quiet(semPlotModel(ef, which = 1))
  m_nf2 <- quiet(semPlotModel(ef, which = "nf2"))
  sum(!m_def@Vars$manifest) == 2 && sum(!m_1@Vars$manifest) == 1 &&
    sum(!m_nf2@Vars$manifest) == 2 })

check("T9c efa() invalid 'which' errors informatively", {
  ef <- quiet(efa(data = HS[paste0("x",1:9)], nfactors = 1:2))
  e <- tryCatch({ semPlotModel(ef, which = "nf9"); NULL }, error = function(e) conditionMessage(e))
  !is.null(e) && grepl("nf", e) })


## ================= item10 =================

# Item 10: psychonetrics importer (lvm + varcov, all subtypes)
if (!(requireNamespace("psychonetrics", quietly = TRUE) && not_cran)) {
  cat("SKIP item10: psychonetrics not installed or running on CRAN\n")
} else {
  pn_L <- matrix(0, 9, 3); pn_L[1:3,1] <- pn_L[4:6,2] <- pn_L[7:9,3] <- 1
  pn_lats <- c("visual","textual","speed")
  pn_lvm_cov <- quiet(psychonetrics::runmodel(psychonetrics::lvm(
    HS[paste0("x",1:9)], lambda = pn_L, latents = pn_lats)))

  check("T10a lvm(cov) imports with expected structure", {
    m <- quiet(semPlotModel(pn_lvm_cov))
    sum(m@Pars$edge == "->") == 9 &&                             # loadings
    sum(m@Pars$edge == "<->" & m@Pars$lhs != m@Pars$rhs) == 3 && # latent covs
    sum(m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs) == 12 &&# variances
    sum(m@Pars$edge == "int") == 9 &&                            # nu (nu_eta fixed at 0, dropped)
    sum(m@Pars$edge == "->" & m@Pars$fixed) == 3 })              # marker loadings kept

  check("T10b lvm(cov) renders", {
    p <- quiet(semPaths(pn_lvm_cov, DoNotPlot = TRUE)); inherits(p, "qgraph") })

  pn_lvm_ggm <- quiet(psychonetrics::runmodel(psychonetrics::lvm(
    HS[paste0("x",1:9)], lambda = pn_L, latents = pn_lats, latent = "ggm")))

  check("T10c lvm(latent=ggm): undirected latent edges, no delta rows", {
    m <- quiet(semPlotModel(pn_lvm_ggm))
    om <- psychonetrics::getmatrix(pn_lvm_ggm, "omega_zeta")
    sum(m@Pars$edge == "--") == sum(om[upper.tri(om)] != 0) &&
    all(m@Pars$lhs[m@Pars$edge == "--"] %in% pn_lats) })

  check("T10d lvm(latent=ggm): latent self-loops equal diag(sigma_zeta)", {
    m <- quiet(semPlotModel(pn_lvm_ggm))
    sz <- psychonetrics::getmatrix(pn_lvm_ggm, "sigma_zeta")
    sl <- m@Pars[m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs & m@Pars$lhs %in% pn_lats, ]
    nrow(sl) == 3 && all(abs(sl$est[match(pn_lats, sl$lhs)] - diag(sz)) < 1e-8) })

  check("T10e lvm(latent=ggm) delta='ignore': no latent self-loops", {
    m <- quiet(semPlotModel(pn_lvm_ggm, delta = "ignore"))
    nrow(m@Pars[m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs & m@Pars$lhs %in% pn_lats, ]) == 0 })

  check("T10f lvm(latent=ggm) renders with undirected edges", {
    p <- quiet(semPaths(pn_lvm_ggm, DoNotPlot = TRUE))
    inherits(p, "qgraph") && sum(!p$Edgelist$directed) >= 1 })

  pn_net <- quiet(psychonetrics::runmodel(psychonetrics::varcov(
    HS[paste0("x",1:4)], type = "ggm")))

  check("T10g varcov(ggm): pure network with variances from implied sigma", {
    m <- quiet(semPlotModel(pn_net))
    sg <- psychonetrics::getmatrix(pn_net, "sigma")
    om <- psychonetrics::getmatrix(pn_net, "omega")
    sum(m@Pars$edge == "--") == sum(om[upper.tri(om)] != 0) &&
    sum(m@Pars$edge == "~>") + sum(m@Pars$edge == "->") == 0 &&
    { sl <- m@Pars[m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs, ]
      all(abs(sl$est[match(paste0("x",1:4), sl$lhs)] - diag(sg)) < 1e-8) } })

  check("T10h varcov(ggm) renders in circle layout, all edges undirected", {
    p <- quiet(semPaths(pn_net, layout = "circle", residuals = FALSE, intercepts = FALSE, DoNotPlot = TRUE))
    inherits(p, "qgraph") && all(!p$Edgelist$directed[p$Edgelist$from != p$Edgelist$to]) })

  check("T10i ggm() wrapper equals varcov(type='ggm')", {
    w <- quiet(psychonetrics::runmodel(psychonetrics::ggm(HS[paste0("x",1:4)])))
    m1 <- quiet(semPlotModel(w)); m2 <- quiet(semPlotModel(pn_net))
    identical(dim(m1@Pars), dim(m2@Pars)) && all(abs(sort(m1@Pars$est) - sort(m2@Pars$est)) < 1e-6) })

  check("T10j multigroup lvm imports with group labels and renders 2 panels", {
    mg <- quiet(psychonetrics::runmodel(psychonetrics::lvm(
      HS, vars = paste0("x",1:9), lambda = pn_L, groups = "school", latents = pn_lats)))
    m <- quiet(semPlotModel(mg))
    p <- quiet(semPaths(mg, DoNotPlot = TRUE, ask = FALSE))
    all(c("Pasteur","Grant-White") %in% m@Pars$group) &&
      is.list(p) && length(p) == 2 && all(sapply(p, inherits, "qgraph")) })

  check("T10k lvm(latent=chol) shows implied latent covariances", {
    ch <- quiet(psychonetrics::runmodel(psychonetrics::lvm(
      HS[paste0("x",1:9)], lambda = pn_L, latents = pn_lats, latent = "chol")))
    m <- quiet(semPlotModel(ch))
    sz <- psychonetrics::getmatrix(ch, "sigma_zeta")
    lc <- m@Pars[m@Pars$edge == "<->" & m@Pars$lhs %in% pn_lats & m@Pars$rhs %in% pn_lats, ]
    nrow(lc) == 6 && !any(m@Pars$edge == "--") })

  check("T10l unsupported framework gives informative error", {
    v <- pn_net; v@model <- "var1"
    e <- tryCatch({ semPlotModel(v); NULL }, error = function(e) conditionMessage(e))
    !is.null(e) && grepl("var1", e) && grepl("supported", e) })

  # --- standardized solution (psychonetrics leaves @parameters$std empty) ---

  check("T10n lvm(latent=ggm): std is populated, not all NA", {
    m <- quiet(semPlotModel(pn_lvm_ggm))
    !all(is.na(m@Pars$std)) && !any(is.na(m@Pars$std)) })

  check("T10o lvm(latent=ggm): std loadings equal lambda * sd_eta / sd_y", {
    m <- quiet(semPlotModel(pn_lvm_ggm))
    lam <- psychonetrics::getmatrix(pn_lvm_ggm, "lambda")
    bet <- psychonetrics::getmatrix(pn_lvm_ggm, "beta")
    sz <- psychonetrics::getmatrix(pn_lvm_ggm, "sigma_zeta")
    sg <- psychonetrics::getmatrix(pn_lvm_ggm, "sigma")
    IB <- solve(diag(nrow(bet)) - bet)
    sdL <- sqrt(diag(IB %*% sz %*% t(IB))); sdY <- sqrt(diag(sg))
    # NB: not named 'ok' -- check() evaluates this in the global frame, where
    # 'ok' is the harness' own pass counter.
    allMatch <- TRUE
    for (i in 1:9) for (j in 1:3) {
      if (lam[i,j] == 0) next
      got <- m@Pars$std[m@Pars$edge == "->" & m@Pars$lhs == pn_lats[j] & m@Pars$rhs == paste0("x",i)]
      allMatch <- allMatch && length(got) == 1 && abs(got - lam[i,j] * sdL[j] / sdY[i]) < 1e-8
    }
    allMatch })

  check("T10p lvm(latent=ggm): omega is already standardized (std == est)", {
    m <- quiet(semPlotModel(pn_lvm_ggm))
    om <- m@Pars[m@Pars$edge == "--", ]
    nrow(om) >= 1 && all(abs(om$std - om$est) < 1e-12) })

  check("T10q lvm(latent=ggm): std residual variances are proportions in [0,1]", {
    m <- quiet(semPlotModel(pn_lvm_ggm))
    se <- psychonetrics::getmatrix(pn_lvm_ggm, "sigma_epsilon")
    sg <- psychonetrics::getmatrix(pn_lvm_ggm, "sigma")
    rv <- m@Pars[m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs & m@Pars$lhs %in% paste0("x",1:9), ]
    all(rv$std >= 0 & rv$std <= 1) &&
      all(abs(rv$std[match(paste0("x",1:9), rv$lhs)] - diag(se)/diag(sg)) < 1e-8) })

  check("T10r lvm(latent=ggm): latent variances standardize to 1 (no latent regressions)", {
    m <- quiet(semPlotModel(pn_lvm_ggm))
    lv <- m@Pars[m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs & m@Pars$lhs %in% pn_lats, ]
    nrow(lv) == 3 && all(abs(lv$std - 1) < 1e-8) })

  check("T10s std intercepts follow the lavaan std.all convention (est / sd_y)", {
    m <- quiet(semPlotModel(pn_lvm_ggm))
    sg <- psychonetrics::getmatrix(pn_lvm_ggm, "sigma")
    it <- m@Pars[m@Pars$edge == "int", ]
    all(abs(it$std[match(paste0("x",1:9), it$rhs)] - it$est[match(paste0("x",1:9), it$rhs)]/sqrt(diag(sg))) < 1e-8) })

  check("T10t semPaths(what='std') renders without non-finite weights", {
    w <- NULL
    p <- withCallingHandlers(quiet(semPaths(pn_lvm_ggm, "std", "est", DoNotPlot = TRUE)),
                             warning = function(x) { w <<- conditionMessage(x); invokeRestart("muffleWarning") })
    inherits(p, "qgraph") && is.null(w) })

  check("T10u varcov(ggm): observed variances standardize to 1", {
    m <- quiet(semPlotModel(pn_net))
    sl <- m@Pars[m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs, ]
    all(abs(sl$std - 1) < 1e-8) })

  # An empty residual network leaves every omega_epsilon fixed at zero; the
  # block must still be recognized as GGM so its delta rows are handled
  # rather than leaking through as unsupported '~/~' parameters.
  check("T10v residual='ggm' with an empty network drops no parameters", {
    rg <- quiet(psychonetrics::runmodel(psychonetrics::lvm(
      HS[paste0("x",1:9)], lambda = pn_L, latents = pn_lats, residual = "ggm")))
    w <- NULL
    m <- withCallingHandlers(semPlotModel(rg),
                             warning = function(x) { w <<- conditionMessage(x); invokeRestart("muffleWarning") })
    is.null(w) && !any(m@Pars$edge == "~/~") && !any(is.na(m@Pars$std)) })

  check("T10w improper solution warns and yields NA std rather than wrong values", {
    bad <- pn_lvm_ggm
    # Force a negative model-implied variance for x1 (a Heywood-like case):
    bad@modelmatrices[[1]]$sigma[1,1] <- -1
    w <- NULL
    m <- withCallingHandlers(semPlotModel(bad),
                             warning = function(x) { w <<- conditionMessage(x); invokeRestart("muffleWarning") })
    std_x1 <- m@Pars$std[m@Pars$edge == "->" & m@Pars$rhs == "x1"]
    !is.null(w) && grepl("improper", w) && grepl("x1", w) &&
      length(std_x1) >= 1 && all(is.na(std_x1)) })

  check("T10m ObsCovs/ImpCovs populated and symmetric", {
    m <- quiet(semPlotModel(pn_lvm_cov))
    length(m@ObsCovs) == 1 && length(m@ImpCovs) == 1 &&
      isSymmetric(unname(m@ObsCovs[[1]])) && nrow(m@ImpCovs[[1]]) == 9 })
}


## ================= item11 =================

# Item 11: undirected (GGM) support in semPaths — defExo symmetry, curveAdjacent, semSyntax guard
mk_ggm_model <- function(){
  Pars <- data.frame(label = "",
    lhs = c("x1","x1","x2","x3", paste0("x",1:4)),
    edge = c(rep("--",4), rep("<->",4)),
    rhs = c("x2","x3","x4","x4", paste0("x",1:4)),
    est = c(.3,-.2,.25,.4, rep(1,4)), std = NA, group = "", fixed = FALSE,
    par = 1:8, stringsAsFactors = FALSE)
  Vars <- data.frame(name = paste0("x",1:4), manifest = TRUE, exogenous = NA,
                     stringsAsFactors = FALSE)
  m <- new("semPlotModel", Pars = Pars, Vars = Vars, Thresholds = data.frame(),
           Computed = TRUE, ObsCovs = list(), ImpCovs = list(), Original = list())
  m
}

check("T11a defExo: pure GGM has symmetric (all-endogenous) layout roles", {
  m <- mk_ggm_model()
  e <- semPlot:::defExo(m, "tree")@Vars$exogenous
  all(e == FALSE) })

check("T11b tree layout puts all pure-GGM nodes on one level", {
  p <- quiet(semPaths(mk_ggm_model(), what = "est", layout = "tree", DoNotPlot = TRUE))
  length(unique(p$layout[,2])) == 1 })

check("T11c same-level non-adjacent -- edge is curved", {
  # 3 nodes on one level; x1--x3 passes over x2 and must curve:
  Pars <- data.frame(label = "", lhs = c("x1","x1","x2"), edge = "--",
                     rhs = c("x2","x3","x3"), est = .3, std = NA, group = "",
                     fixed = FALSE, par = 1:3, stringsAsFactors = FALSE)
  Vars <- data.frame(name = paste0("x",1:3), manifest = TRUE, exogenous = NA,
                     stringsAsFactors = FALSE)
  m <- new("semPlotModel", Pars = Pars, Vars = Vars, Thresholds = data.frame(),
           Computed = TRUE, ObsCovs = list(), ImpCovs = list(), Original = list())
  p <- quiet(semPaths(m, what = "est", layout = "tree", DoNotPlot = TRUE))
  # find the x1--x3 edge (nodes 1 and 3 in layout order) and check curvature:
  ce <- p$graphAttributes$Edges$curve
  el <- cbind(p$Edgelist$from, p$Edgelist$to)
  far <- which((el[,1] == 1 & el[,2] == 3) | (el[,1] == 3 & el[,2] == 1))
  length(far) == 1 && abs(ce[far]) > 0 })

check("T11d semSyntax warns and skips -- edges", {
  m <- mk_ggm_model()
  w <- NULL
  s <- withCallingHandlers(
    capture.output(res <- semSyntax(m, "lavaan")),
    warning = function(cond){ w <<- conditionMessage(cond); invokeRestart("muffleWarning") })
  !is.null(w) && grepl("Undirected", w) && !grepl("--", res) })

check("T11e factanal loadings model unaffected by defExo change", {
  fa <- factanal(HS[paste0("x",1:9)], factors = 3)
  m <- quiet(semPlotModel(fa$loadings))
  e <- semPlot:::defExo(m, "tree")@Vars$exogenous
  # Historical behavior (verified pre-change): the two role loops mark everything
  # exogenous, and the all-exo reset flips everything to endogenous — the MIMIC
  # branch never fires for loadings models under either the old or new edge sets.
  all(e == FALSE) &&
    inherits(quiet(semPaths(m, what = "est", DoNotPlot = TRUE)), "qgraph") })


## ================= item14 =================
# Item 14: importer registration API + model validator
check("T14a registered importer is dispatched by semPlotModel/semPaths", {
  toy <- function(object, ...){
    Pars <- data.frame(label = "", lhs = "A", edge = "~>", rhs = "B", est = 0.5,
                       std = NA, group = "", fixed = FALSE, par = 1,
                       stringsAsFactors = FALSE)
    Vars <- data.frame(name = c("A","B"), manifest = TRUE, exogenous = NA,
                       stringsAsFactors = FALSE)
    m <- new("semPlotModel", Pars = Pars, Vars = Vars, Thresholds = data.frame(),
             Computed = TRUE, ObsCovs = list(), ImpCovs = list(), Original = list())
    m
  }
  registerSemPlotImporter("myToyModel", toy)
  obj <- structure(list(), class = "myToyModel")
  m <- semPlotModel(obj)
  p <- quiet(semPaths(obj, DoNotPlot = TRUE))
  inherits(m, "semPlotModel") && nrow(m@Pars) == 1 && inherits(p, "qgraph") })

check("T14b validator catches unknown edge types", {
  Pars <- data.frame(label = "", lhs = "A", edge = "=>", rhs = "B", est = 1,
                     std = NA, group = "", fixed = FALSE, par = 1, stringsAsFactors = FALSE)
  Vars <- data.frame(name = c("A","B"), manifest = TRUE, exogenous = NA, stringsAsFactors = FALSE)
  bad <- new("semPlotModel", Pars = Pars, Vars = Vars, Thresholds = data.frame(),
             Computed = TRUE, ObsCovs = list(), ImpCovs = list(), Original = list())
  e <- tryCatch({ validateSemPlotModel(bad); NULL }, error = function(e) conditionMessage(e))
  !is.null(e) && grepl("Unknown edge", e) })

check("T14c validator catches Pars referring to unknown variables", {
  Pars <- data.frame(label = "", lhs = "A", edge = "~>", rhs = "C", est = 1,
                     std = NA, group = "", fixed = FALSE, par = 1, stringsAsFactors = FALSE)
  Vars <- data.frame(name = c("A","B"), manifest = TRUE, exogenous = NA, stringsAsFactors = FALSE)
  bad <- new("semPlotModel", Pars = Pars, Vars = Vars, Thresholds = data.frame(),
             Computed = TRUE, ObsCovs = list(), ImpCovs = list(), Original = list())
  e <- tryCatch({ validateSemPlotModel(bad); NULL }, error = function(e) conditionMessage(e))
  !is.null(e) && grepl("not in Vars", e) })

check("T14d validator passes a real imported model", {
  m <- quiet(semPlotModel(fit_cfa))
  identical(validateSemPlotModel(m), m) })

## ================= item15a =================
# Item 15a: psych fa()/omega() importers
if (!requireNamespace("psych", quietly = TRUE)) {
  cat("SKIP item15a: psych not installed\n")
} else {
  fa3 <- quiet(psych::fa(HS[paste0("x",1:9)], nfactors = 3, rotate = "oblimin", fm = "ml"))

  check("T15a1 fa() imports: loadings, Phi, uniquenesses", {
    m <- quiet(semPlotModel(fa3))
    sum(m@Pars$edge == "->") == 27 &&
      sum(m@Pars$edge == "<->" & m@Pars$lhs != m@Pars$rhs) == 3 &&
      sum(m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs) == 9 &&
      sum(!m@Vars$manifest) == 3 })

  check("T15a2 fa() renders", {
    inherits(quiet(semPaths(fa3, DoNotPlot = TRUE)), "qgraph") })

  check("T15a3 fa() varimax: no factor correlations", {
    fv <- quiet(psych::fa(HS[paste0("x",1:9)], nfactors = 2, rotate = "varimax", fm = "ml"))
    m <- quiet(semPlotModel(fv))
    sum(m@Pars$edge == "<->" & m@Pars$lhs != m@Pars$rhs) == 0 })

  check("T15a4 fa() cut argument drops small loadings", {
    m0 <- quiet(semPlotModel(fa3))
    m3 <- quiet(semPlotModel(fa3, cut = 0.3))
    sum(m3@Pars$edge == "->") < sum(m0@Pars$edge == "->") &&
      all(abs(m3@Pars$est[m3@Pars$edge == "->"]) >= 0.3) })

  check("T15a5 omega() imports bifactor with g first and renders", {
    om <- quiet(psych::omega(HS[paste0("x",1:9)], nfactors = 3, plot = FALSE))
    m <- quiet(semPlotModel(om, cut = 0.01))
    lats <- m@Vars$name[!m@Vars$manifest]
    gload <- m@Pars[m@Pars$edge == "->" & m@Pars$lhs == "g", ]
    lats[1] == "g" && nrow(gload) >= 8 &&
      sum(m@Pars$edge == "<->" & m@Pars$lhs == m@Pars$rhs) == 9 &&
      sum(m@Pars$edge == "<->" & m@Pars$lhs != m@Pars$rhs) == 0 &&
      inherits(quiet(semPaths(om, bifactor = "g", layout = "tree2", DoNotPlot = TRUE)), "qgraph") })

  check("T15a6 principal() still works via delegation", {
    pc <- quiet(psych::principal(HS[paste0("x",1:9)], nfactors = 2))
    m <- quiet(semPlotModel(pc))
    inherits(m, "semPlotModel") })
}

## ================= item15b =================
# Item 15b: piecewiseSEM psem importer
if (!requireNamespace("piecewiseSEM", quietly = TRUE)) {
  cat("SKIP item15b: piecewiseSEM not installed\n")
} else {
  data(keeley, package = "piecewiseSEM")

  check("T15b1 psem imports directed paths matching coefs()", {
    mod <- quiet(piecewiseSEM::psem(
      lm(rich ~ cover, data = keeley),
      lm(cover ~ firesev, data = keeley),
      lm(firesev ~ age, data = keeley),
      data = keeley))
    co <- quiet(piecewiseSEM::coefs(mod))
    m <- quiet(semPlotModel(mod))
    dir <- m@Pars[m@Pars$edge == "~>", ]
    nrow(dir) == 3 &&
      all(abs(sort(dir$est) - sort(as.numeric(co$Estimate))) < 1e-10) &&
      all(!is.na(dir$std)) })

  check("T15b2 psem renders", {
    mod <- quiet(piecewiseSEM::psem(
      lm(rich ~ cover, data = keeley),
      lm(cover ~ firesev, data = keeley),
      data = keeley))
    inherits(quiet(semPaths(mod, DoNotPlot = TRUE)), "qgraph") })

  check("T15b3 correlated errors become <-> edges", {
    `%~~%` <- piecewiseSEM::`%~~%`
    mod <- quiet(piecewiseSEM::psem(
      lm(rich ~ firesev, data = keeley),
      lm(cover ~ firesev, data = keeley),
      rich %~~% cover,
      data = keeley))
    m <- quiet(semPlotModel(mod))
    ce <- m@Pars[m@Pars$edge == "<->", ]
    nrow(ce) == 1 && sort(c(ce$lhs, ce$rhs))[1] == "cover" &&
      !grepl("~~", paste(ce$lhs, ce$rhs)) })

  check("T15b4 glm component supported", {
    k2 <- keeley; k2$rich01 <- as.numeric(k2$rich > stats::median(k2$rich))
    mod <- quiet(piecewiseSEM::psem(
      glm(rich01 ~ cover, family = binomial, data = k2),
      lm(cover ~ firesev, data = k2),
      data = k2))
    m <- quiet(semPlotModel(mod))
    sum(m@Pars$edge == "~>") == 2 })
}

## ================= item13 =================
# Item 13: lavaan.mi pooled multiple-imputation importer
if (!requireNamespace("lavaan.mi", quietly = TRUE)) {
  cat("SKIP item13: lavaan.mi not installed\n")
} else {
  mi_fit <- local({
    set.seed(11)
    d <- HS[paste0("x",1:6)]
    d[sample(nrow(d), 30), "x1"] <- NA
    imps <- lapply(1:3, function(i){ di <- d
      for (v in names(di)){ na <- is.na(di[[v]])
        di[[v]][na] <- mean(di[[v]], na.rm = TRUE) + rnorm(sum(na), 0, sd(di[[v]], na.rm = TRUE)) }
      di })
    quiet(lavaan.mi::cfa.mi(" visual =~ x1+x2+x3\n textual =~ x4+x5+x6 ", data = imps))
  })

  check("T13a lavaan.mi imports with pooled estimates", {
    m <- quiet(semPlotModel(mi_fit))
    pe <- quiet(lavaan.mi::parameterEstimates.mi(mi_fit))
    inherits(m, "semPlotModel") && nrow(m@Pars) == nrow(pe) &&
      all(abs(sort(m@Pars$est) - sort(pe$est)) < 1e-10) })

  check("T13b lavaan.mi std column pools standardizedSolution.mi", {
    m <- quiet(semPlotModel(mi_fit))
    ss <- quiet(lavaan.mi::standardizedSolution.mi(mi_fit))
    ld <- m@Pars[m@Pars$edge == "->", ]
    rows_ok <- sapply(seq_len(nrow(ld)), function(i){
      row <- ss[ss$lhs == ld$lhs[i] & ss$op == "=~" & ss$rhs == ld$rhs[i], ]
      nrow(row) == 1 && abs(row$est.std - ld$std[i]) < 1e-10 })
    all(rows_ok) })

  check("T13c lavaan.mi renders and semCors errors informatively", {
    p <- quiet(semPaths(mi_fit, DoNotPlot = TRUE))
    e <- tryCatch({ semCors(quiet(semPlotModel(mi_fit))); NULL },
                  error = function(e) conditionMessage(e))
    inherits(p, "qgraph") && !is.null(e) && grepl("covariance", e) })

  check("T13d lavaan.mi edge structure matches complete-data fit", {
    fitc <- quiet(cfa(" visual =~ x1+x2+x3\n textual =~ x4+x5+x6 ", data = HS))
    mc <- quiet(semPlotModel(fitc)); mm <- quiet(semPlotModel(mi_fit))
    identical(table(mc@Pars$edge), table(mm@Pars$edge)) })
}

## ================= item15c =================
# Item 15c: umx compatibility (umxRAM returns MxRAMModel; OpenMx importer handles it)
not_cran0 <- function() !exists("not_cran") || isTRUE(not_cran)
if (!(requireNamespace("umx", quietly = TRUE) && not_cran0())) {
  cat("SKIP item15c: umx not installed or CRAN mode\n")
} else {
  # umx initializes its options in .onAttach, so attach it (as its users do):
  suppressMessages(require(umx, quietly = TRUE))
  check("T15c1 umxRAM model imports and renders via OpenMx importer", {
    HSu <- HS[paste0("x",1:3)]
    m1 <- quiet(umxRAM("m1", data = HSu,
      umxPath(from = "vis", to = paste0("x",1:3)),
      umxPath(var = paste0("x",1:3)),
      umxPath(var = "vis", fixedAt = 1),
      umxPath(means = paste0("x",1:3)),
      autoRun = TRUE))
    m <- quiet(semPlotModel(m1))
    p <- quiet(semPaths(m1, DoNotPlot = TRUE))
    inherits(m, "semPlotModel") && sum(m@Pars$edge == "->") == 3 && inherits(p, "qgraph") })
}

## ================= item15d =================
# Item 15d: metaSEM stage-2 (wls) importer via meta2semPlot
not_cran2 <- function() !exists("not_cran") || isTRUE(not_cran)
if (!(requireNamespace("metaSEM", quietly = TRUE) && not_cran2())) {
  cat("SKIP item15d: metaSEM not installed or CRAN mode\n")
} else {
  ms_fit2 <- tryCatch({
    f1 <- quiet(metaSEM::tssem1(metaSEM::Becker92$data, metaSEM::Becker92$n, method = "FEM"))
    A <- metaSEM::create.mxMatrix(c(0, "0.2*Spatial2Math", "0.2*Verbal2Math",
                                    0, 0, 0,
                                    0, 0, 0),
                                  type = "Full", ncol = 3, nrow = 3, byrow = TRUE, name = "A")
    S <- metaSEM::create.mxMatrix(c("0.2*ErrMath",
                                    0, 1,
                                    0, "0.2*SpatialVerbal", 1),
                                  type = "Symm", byrow = TRUE, ncol = 3, nrow = 3, name = "S")
    quiet(metaSEM::tssem2(f1, Amatrix = A, Smatrix = S, diag.constraints = FALSE))
  }, error = function(e) NULL)

  if (is.null(ms_fit2)) {
    cat("SKIP item15d: could not fit the metaSEM example\n")
  } else {
    check("T15d1 metaSEM wls imports via meta2semPlot and renders", {
      m <- quiet(semPlotModel(ms_fit2))
      inherits(m, "semPlotModel") && nrow(m@Pars) >= 4 &&
        inherits(quiet(semPaths(ms_fit2, DoNotPlot = TRUE)), "qgraph") })

    check("T15d2 metaSEM stage-1 object errors informatively", {
      f1 <- quiet(metaSEM::tssem1(metaSEM::Becker92$data, metaSEM::Becker92$n, method = "FEM"))
      e <- tryCatch({ semPlotModel(f1); NULL }, error = function(e) conditionMessage(e))
      !is.null(e) && grepl("stage-1", e) })
  }
}

## ================= item12 =================
# Item 12: blavaan support without the class-overwrite hack
not_cran3 <- function() !exists("not_cran") || isTRUE(not_cran)
if (!(requireNamespace("blavaan", quietly = TRUE) && not_cran3())) {
  cat("SKIP item12: blavaan not installed or CRAN mode\n")
} else {
  suppressMessages(require(blavaan, quietly = TRUE))
  fit_blav <- local({
    f <- function(){
      sink(tempfile()); on.exit(sink(), add = TRUE)
      quiet(blavaan::bcfa(" visual =~ x1 + x2 + x3 ", data = HS,
                          burnin = 100, sample = 100))
    }
    tryCatch(f(), error = function(e) { cat("note: blavaan fit error:", conditionMessage(e), "\n"); NULL })
  })
  if (is.null(fit_blav)) {
    cat("SKIP item12: blavaan example fit failed\n")
  } else {
    check("T12a blavaan imports with class intact and posterior std", {
      m <- quiet(semPlotModel(fit_blav))
      is(m@Original[[1]], "blavaan") &&
        sum(!is.na(m@Pars$std)) == nrow(m@Pars) &&
        inherits(m, "semPlotModel") })
    check("T12b blavaan matches lavaan edge structure and renders", {
      fitl <- quiet(cfa(" visual =~ x1 + x2 + x3 ", data = HS))
      ml <- quiet(semPlotModel(fitl)); mb <- quiet(semPlotModel(fit_blav))
      identical(table(ml@Pars$edge), table(mb@Pars$edge)) &&
        inherits(quiet(semPaths(fit_blav, DoNotPlot = TRUE)), "qgraph") })
  }
}

## ================= item15e =================
# Item 15e: PLS-SEM importers (seminr, cSEM)
not_cran4 <- function() !exists("not_cran") || isTRUE(not_cran)
if (!(requireNamespace("seminr", quietly = TRUE) && not_cran4())) {
  cat("SKIP item15e (seminr): not installed or CRAN mode\n")
} else {
  sem_fit <- quiet({
    mm <- seminr::constructs(
      seminr::composite("Image", seminr::multi_items("IMAG", 1:5)),
      seminr::composite("Satisfaction", seminr::multi_items("CUSA", 1:3), weights = seminr::mode_B),
      seminr::reflective("Loyalty", seminr::multi_items("CUSL", 1:3)))
    sm <- seminr::relationships(
      seminr::paths(from = "Image", to = c("Satisfaction","Loyalty")),
      seminr::paths(from = "Satisfaction", to = "Loyalty"))
    seminr::estimate_pls(data = seminr::mobi, measurement_model = mm, structural_model = sm)
  })
  check("T15e1 seminr: modes map to loading vs weight arrows; renders", {
    m <- quiet(semPlotModel(sem_fit))
    modeB <- m@Pars[m@Pars$edge == "~>" & m@Pars$rhs == "Satisfaction" & m@Pars$lhs %in% paste0("CUSA",1:3), ]
    refl <- m@Pars[m@Pars$edge == "->" & m@Pars$lhs %in% c("Image","Loyalty"), ]
    struct <- m@Pars[m@Pars$edge == "~>" & m@Pars$lhs %in% c("Image","Satisfaction") & m@Pars$rhs %in% c("Satisfaction","Loyalty"), ]
    nrow(modeB) == 3 && nrow(refl) == 8 && nrow(struct) == 3 &&
      inherits(quiet(semPaths(sem_fit, DoNotPlot = TRUE)), "qgraph") })
}
if (!(requireNamespace("cSEM", quietly = TRUE) && not_cran4())) {
  cat("SKIP item15e (cSEM): not installed or CRAN mode\n")
} else {
  csem_fit <- quiet({
    model <- "
      EXPE ~ IMAG
      SAT ~ EXPE
      IMAG =~ imag1 + imag2 + imag3
      EXPE =~ expe1 + expe2 + expe3
      SAT <~ sat1 + sat2 + sat3
    "
    cSEM::csem(.data = cSEM::satisfaction[, c(paste0("imag",1:3), paste0("expe",1:3), paste0("sat",1:3))],
               .model = model)
  })
  check("T15e2 cSEM: composites get weights, common factors loadings; renders", {
    m <- quiet(semPlotModel(csem_fit))
    watSAT <- m@Pars[m@Pars$edge == "~>" & m@Pars$rhs == "SAT" & grepl("^sat", m@Pars$lhs), ]
    loads <- m@Pars[m@Pars$edge == "->" & m@Pars$lhs %in% c("IMAG","EXPE"), ]
    noSATload <- !any(m@Pars$edge == "->" & m@Pars$lhs == "SAT")
    struct <- m@Pars[m@Pars$edge == "~>" & m@Pars$lhs %in% c("IMAG","EXPE") & m@Pars$rhs %in% c("EXPE","SAT"), ]
    nrow(watSAT) == 3 && nrow(loads) == 6 && noSATload && nrow(struct) == 2 &&
      inherits(quiet(semPaths(csem_fit, DoNotPlot = TRUE)), "qgraph") })
}

cat("\n==== RESULT:", ok, "passed,", fail, "failed ====\n")
if (fail > 0) stop("semPlot regression tests failed: ", paste(fails, collapse = "; "))

Try the semPlot package in your browser

Any scripts or data that you put into this service are public.

semPlot documentation built on July 18, 2026, 1:06 a.m.