diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 35f45f8..fd1d3c3 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -190,6 +190,30 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa ui_elements }) + # Spectronaut intensity column input — universal across both the + # regular (in-memory) and large-file paths, regardless of analysis + # template. Default tracks the template: turnover analyses want the + # MS1-only quantity, normal analyses want the normalized peak area + # (which is also `bigSpectronauttoMSstatsFormat`'s default). + output$spectronaut_intensity_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + + default_intensity <- if (!is.null(app_template) && + app_template() == TEMPLATES$protein_turnover) { + "FG.MS1Quantity" + } else { + "F.NormalizedPeakArea" + } + + textInput(session$ns("spec_intensity_col"), + label = h5("Intensity column", + class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Spectronaut export column to use as the intensity measure (e.g. F.NormalizedPeakArea, F.PeakArea, FG.MS1Quantity). Leave at the default unless you have a specific reason to override it.", + class = "icon-tooltip")), + value = default_intensity) + }) + output$spectronaut_turnover_ui <- renderUI({ req(input$filetype == 'spec', input$BIO != 'PTM') req(!is.null(app_template) && app_template() == TEMPLATES$protein_turnover) @@ -198,9 +222,6 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa tagList( tags$hr(), h4("Protein Turnover Options"), - textInput(ns("spec_intensity_col"), - "Intensity column", - value = "FG.MS1Quantity"), textInput(ns("spec_peptide_seq_col"), "Peptide sequence column", value = "FG.LabeledSequence"), @@ -235,11 +256,13 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa unique_peps_def <- if (is.null(input$filter_unique_peptides)) FALSE else input$filter_unique_peptides agg_psms_def <- if (is.null(input$aggregate_psms)) FALSE else input$aggregate_psms few_obs_def <- if (is.null(input$filter_few_obs)) FALSE else input$filter_few_obs - + calculate_anomaly_def <- if (is.null(input$calculate_anomaly_scores)) FALSE else input$calculate_anomaly_scores + tagList( create_spectronaut_large_filter_options(session$ns, excluded_def, identified_def, qval_def), if (qval_def) create_spectronaut_qvalue_cutoff_ui(session$ns, cutoff_def), - create_spectronaut_large_bottom_ui(session$ns, max_feature_def, unique_peps_def, agg_psms_def, few_obs_def) + create_spectronaut_large_bottom_ui(session$ns, max_feature_def, unique_peps_def, agg_psms_def, few_obs_def), + create_spectronaut_large_annotation_ui(session$ns, calculate_anomaly_def) ) } else { NULL diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index 2de45d0..44e5ea3 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -282,6 +282,7 @@ create_spectronaut_uploads <- function(ns) { uiOutput(ns("spectronaut_header_ui")), uiOutput(ns("spectronaut_file_selection_ui")), uiOutput(ns("spectronaut_options_ui")), + uiOutput(ns("spectronaut_intensity_ui")), uiOutput(ns("spectronaut_turnover_ui")) ) } @@ -342,6 +343,61 @@ create_spectronaut_large_bottom_ui <- function(ns, max_feature_def = 20, unique_ ) } +#' Create Spectronaut large file annotation override + anomaly UI +#' +#' Renders an optional annotation upload that overrides Spectronaut's embedded +#' R.Condition / R.Replicate columns on Run, plus the "Calculate Anomaly +#' Scores" controls. End-to-end anomaly scoring is a two-step pipeline in +#' the large-file path: +#' (1) `bigSpectronauttoMSstatsFormat` runs with +#' `calculateAnomalyScores = TRUE` + the model feature column list, +#' which carries those feature columns through the out-of-memory +#' reduce/preprocess steps. +#' (2) After `dplyr::collect`, `MSstatsConvert::MSstatsAnomalyScores` +#' is called on the in-memory result to fit the isolation-forest +#' model and produce the `AnomalyScores` column. +#' Input IDs `calculate_anomaly_scores` and `run_order_file` are deliberately +#' the same as the regular Spectronaut path's so downstream pages +#' (module-qc-server's MSstats+ summarization gate, getDataCode's +#' reproducibility script, etc.) read a single source of truth regardless +#' of which path the user took. The two UI checkboxes never coexist — +#' the regular path's `create_label_free_options` is hidden when +#' `big_file_spec` is on, and this helper only renders when it is — so +#' there is no Shiny namespace collision. +#' A run-order CSV is required (Run + Order columns) — `MSstatsAnomalyScores` +#' uses it for temporal feature engineering. +#' @noRd +create_spectronaut_large_annotation_ui <- function(ns, calculate_anomaly_def = FALSE) { + tagList( + tags$hr(), + h5("Annotation file (optional)", + class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Upload a CSV/TSV with columns Run, BioReplicate, Condition (and any extras). When supplied, the converter merges it on Run and overrides any Condition / BioReplicate values from Spectronaut's R.Condition / R.Replicate. Required for paired designs and other layouts Spectronaut's own annotation cannot express.", + class = "icon-tooltip")), + fileInput(ns("big_spec_annotation"), label = NULL, + multiple = FALSE, accept = c(".csv", ".tsv", ".txt")), + checkboxInput(ns("calculate_anomaly_scores"), + label = tags$span( + "Calculate Anomaly Scores", + class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Runs the same anomaly scoring pipeline as the regular Spectronaut path: the converter carries FG.ShapeQualityScore (MS2)/(MS1) and EGDeltaRT through the out-of-memory steps, then MSstatsConvert::MSstatsAnomalyScores fits the isolation-forest model on the collected data and adds an AnomalyScores column. Requires a run order CSV.", + class = "icon-tooltip")), + value = calculate_anomaly_def), + conditionalPanel( + condition = sprintf("input['%s']", ns("calculate_anomaly_scores")), + fileInput(ns("run_order_file"), + label = h5("Upload Run Order File", + class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("CSV with two columns: 'Run' (sequence name matching the converter output) and 'Order' (chronological run number, e.g. 1, 2, 3...).", + class = "icon-tooltip")), + multiple = FALSE, accept = c(".csv")) + ) + ) +} + #' Create PTM FragPipe uploads #' @noRd create_ptm_fragpipe_uploads <- function(ns) { diff --git a/R/utils.R b/R/utils.R index cf9ea76..8b41a8d 100644 --- a/R/utils.R +++ b/R/utils.R @@ -639,11 +639,23 @@ getData <- function(input) { shinybusy::remove_modal_spinner() return(NULL) } - + + if (isTRUE(input$calculate_anomaly_scores) && is.null(input$run_order_file)) { + showNotification( + "Error: Run Order CSV is required when Calculate Anomaly Scores is enabled. Please upload a CSV with Run and Order columns.", + type = "error", + duration = NULL) + shinybusy::remove_modal_spinner() + return(NULL) + } + shinybusy::update_modal_spinner(text = "Processing large Spectronaut file...") - - # Call the big file conversion function from MSstatsConvert - converted_data <- MSstatsBig::bigSpectronauttoMSstatsFormat( + + # Base arguments shared by every large-file Spectronaut run. + # Optional args (annotation override, anomaly-feature + # carry-through) are spliced in below so callers that don't + # supply them aren't forced to pass NULL / FALSE explicitly. + big_spec_args <- list( input_file = local_big_file_path, output_file_name = "output_file.csv", backend = "arrow", @@ -656,6 +668,27 @@ getData <- function(input) { aggregate_psms = input$aggregate_psms, filter_few_obs = input$filter_few_obs ) + + if (!is.null(input$spec_intensity_col) && + nchar(trimws(input$spec_intensity_col)) > 0) { + big_spec_args$intensity <- trimws(input$spec_intensity_col) + } + + if (!is.null(input$big_spec_annotation)) { + big_spec_args$annotation <- data.table::fread( + input$big_spec_annotation$datapath) + } + + if (isTRUE(input$calculate_anomaly_scores)) { + big_spec_args$calculateAnomalyScores <- TRUE + big_spec_args$anomalyModelFeatures <- c( + "FG.ShapeQualityScore (MS2)", + "FG.ShapeQualityScore (MS1)", + "EG.DeltaRT") + } + + converted_data <- do.call( + MSstatsBig::bigSpectronauttoMSstatsFormat, big_spec_args) # Attempt to load the data into memory. mydata <- tryCatch({ @@ -673,8 +706,37 @@ getData <- function(input) { shinybusy::remove_modal_spinner() return(NULL) } - + + if (isTRUE(input$calculate_anomaly_scores) && + !is.null(input$run_order_file)) { + run_order <- data.table::fread(input$run_order_file$datapath) + mydata <- MSstatsConvert::MSstatsAnomalyScores( + input = mydata, + quality_metrics = c("FGShapeQualityScore(MS2)", + "FGShapeQualityScore(MS1)", + "EGDeltaRT"), + temporal_direction = c("mean_decrease", + "mean_decrease", + "dispersion_increase"), + missing_run_count = 0.5, + n_feat = 100, + run_order = run_order, + n_trees = 100, + max_depth = "auto", + cores = 1) + } + } else { + + if (isTRUE(input$calculate_anomaly_scores) && is.null(input$run_order_file)) { + showNotification( + "Error: Run Order CSV is required when Calculate Anomaly Scores is enabled. Please upload a CSV with Run and Order columns.", + type = "error", + duration = NULL) + remove_modal_spinner() + return(NULL) + } + data = data.table::fread(input$specdata$datapath) # Base arguments for the Spectronaut converter converter_args = list( @@ -958,14 +1020,87 @@ library(MSstatsPTM)\n", sep = "") } else if(input$filetype == 'spec') { + if (isTRUE(input$big_file_spec)) { + codes = paste(codes, + "# Large-file (out-of-memory) Spectronaut path.\n", + "input_file = \"insert your raw Spectronaut export filepath\"\n", + sep = "") + + big_spec_extra <- "" + if (!is.null(input$spec_intensity_col) && + nchar(trimws(input$spec_intensity_col)) > 0) { + big_spec_extra <- paste0(big_spec_extra, + ",\n intensity = \"", + trimws(input$spec_intensity_col), "\"") + } + if (!is.null(input$big_spec_annotation)) { + codes = paste(codes, + "annot_file = data.table::fread(\"insert your annotation filepath (Run, BioReplicate, Condition)\")\n", + sep = "") + big_spec_extra <- paste0(big_spec_extra, + ",\n annotation = annot_file") + } + if (isTRUE(input$calculate_anomaly_scores)) { + big_spec_extra <- paste0(big_spec_extra, + ",\n calculateAnomalyScores = TRUE", + ",\n anomalyModelFeatures = c(\"FG.ShapeQualityScore (MS2)\", \"FG.ShapeQualityScore (MS1)\", \"EG.DeltaRT\")") + } + + codes = paste(codes, + "converted = MSstatsBig::bigSpectronauttoMSstatsFormat(input_file, + output_file_name = \"output_file.csv\", + backend = \"arrow\", + filter_by_excluded = ", input$filter_by_excluded, ", + filter_by_identified = ", input$filter_by_identified, ", + filter_by_qvalue = ", input$filter_by_qvalue, ", + qvalue_cutoff = ", input$qvalue_cutoff, ", + max_feature_count = ", input$max_feature_count, ", + filter_unique_peptides = ", input$filter_unique_peptides, ", + aggregate_psms = ", input$aggregate_psms, ", + filter_few_obs = ", input$filter_few_obs, + big_spec_extra, + ")\ndata = dplyr::collect(converted)\n", + sep = "") + + if (isTRUE(input$calculate_anomaly_scores)) { + codes = paste(codes, + "# Step 2 of the anomaly scoring pipeline: fit the\n", + "# isolation-forest model on the collected data and\n", + "# add an AnomalyScores column.\n", + "run_order = data.table::fread(\"insert your run order CSV filepath (Run, Order columns)\")\n", + "data = MSstatsConvert::MSstatsAnomalyScores(\n", + " input = data,\n", + " # Standardized column names (raw Spectronaut names\n", + " # had `.` and ` ` stripped during the converter step).\n", + " quality_metrics = c(\"FGShapeQualityScore(MS2)\", \"FGShapeQualityScore(MS1)\", \"EGDeltaRT\"),\n", + " temporal_direction = c(\"mean_decrease\", \"mean_decrease\", \"dispersion_increase\"),\n", + " missing_run_count = 0.5,\n", + " n_feat = 100,\n", + " run_order = run_order,\n", + " n_trees = 100,\n", + " max_depth = \"auto\",\n", + " cores = 1)\n", + sep = "") + } + + } else { + codes = paste(codes, "data = data.table::fread(\"insert your MSstats scheme output from Spectronaut filepath\")\nannot_file = data.table::fread(\"insert your annotation filepath\")#Optional\n" , sep = "") + reg_spec_intensity_arg <- if (!is.null(input$spec_intensity_col) && + nchar(trimws(input$spec_intensity_col)) > 0) { + paste0(" intensity = \"", + trimws(input$spec_intensity_col), "\",\n") + } else { + "" + } + if (isTRUE(input$calculate_anomaly_scores)) { codes = paste(codes, "run_order = data.table::fread(\"insert your run order CSV filepath (Run, Order columns)\")\n", sep = "") codes = paste(codes, "data = SpectronauttoMSstatsFormat(data, annotation = annot_file, #Optional - filter_with_Qvalue = ", input$q_val, ", +", reg_spec_intensity_arg, " filter_with_Qvalue = ", input$q_val, ", qvalue_cutoff = ", input$q_cutoff, ", removeProtein_with1Feature = ", input$remove, ", use_log_file = FALSE, @@ -979,11 +1114,13 @@ library(MSstatsPTM)\n", sep = "") } else { codes = paste(codes, "data = SpectronauttoMSstatsFormat(data, annotation = annot_file, #Optional - filter_with_Qvalue = ", input$q_val, ", +", reg_spec_intensity_arg, " filter_with_Qvalue = ", input$q_val, ", qvalue_cutoff = ", input$q_cutoff, ", removeProtein_with1Feature = ", input$remove, ", use_log_file = FALSE)\n", sep = "") } + + } } else if(input$filetype == 'diann') { diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index cf0b13a..3ed7d6c 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -1485,12 +1485,45 @@ describe("getData for Spectronaut input with anomaly scores", { #EXECUTION result_args <- getData(mock_input_no_anomaly) - + #ASSERTION: Check that the anomaly arguments are NOT present expect_null(result_args$calculateAnomalyScores) expect_null(result_args$runOrder) expect_null(result_args$anomalyModelFeatures) }) + + test_that("fails fast when calculate_anomaly_scores is TRUE but run_order_file is missing (regular path)", { + mock_input_missing_runorder <- list( + BIO = "Protein", + DDA_DIA = "DIA", + filetype = "spec", + specdata = list(datapath = "dummy_spec.csv"), + annot = list(datapath = "dummy_annot.csv"), + q_val = TRUE, + q_cutoff = 0.01, + remove = TRUE, + calculate_anomaly_scores = TRUE, + run_order_file = NULL + ) + + stub(getData, "showNotification", + function(msg, ...) expect_match(msg, "Run Order CSV")) + # getData starts with show_modal_spinner() — the validation + # must call remove_modal_spinner() before returning NULL so + # the spinner doesn't get stuck. Track that it was called. + spinner_removed <- FALSE + stub(getData, "remove_modal_spinner", + function(...) { spinner_removed <<- TRUE; NULL }) + # The converter should never run; if it does, fail the test. + stub(getData, "data.table::fread", + function(...) stop("fread reached despite missing run order")) + stub(getData, "SpectronauttoMSstatsFormat", + function(...) stop("converter reached despite missing run order")) + + res <- getData(mock_input_missing_runorder) + expect_null(res) + expect_true(spinner_removed) + }) }) describe("getData for Big Spectronaut", { @@ -1577,10 +1610,202 @@ describe("getData for Big Spectronaut", { stub(getData, "showNotification", function(msg, ...) expect_match(msg, "Memory Error")) stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) - + res <- getData(mock_input_big) expect_null(res) }) + + # Capturing converter (returns its args so we can inspect what + # got forwarded). Same idea as mock_spectro_converter above; the + # big-file caller uses do.call(), but mockery intercepts the + # MSstatsBig::bigSpectronauttoMSstatsFormat symbol resolution + # rather than the call form, so this still works. + mock_big_spec_converter <- function(...) list(...) + dummy_annot_df <- data.frame( + Run = c("run1", "run2"), + BioReplicate = c(7L, 8L), + Condition = c("ctrl", "treat"), + stringsAsFactors = FALSE) + + test_that("passes annotation to converter when big_spec_annotation is supplied", { + input_with_annot <- mock_input_big + input_with_annot$big_spec_annotation <- list(datapath = "annot.csv") + + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + stub(getData, "showNotification", function(...) NULL) + stub(getData, "data.table::fread", dummy_annot_df) + stub(getData, "MSstatsBig::bigSpectronauttoMSstatsFormat", + mock_big_spec_converter) + # Hijack dplyr::collect to read back what the (stubbed) + # converter received — getData passes its return value into + # collect, so the captured value IS the list of args. + captured_args <- NULL + stub(getData, "dplyr::collect", function(x) { + captured_args <<- x + mock_df + }) + + getData(input_with_annot) + + expect_true(!is.null(captured_args$annotation)) + expect_equal(captured_args$annotation, dummy_annot_df) + }) + + test_that("passes calculateAnomalyScores + anomalyModelFeatures to converter when calculate_anomaly_scores = TRUE", { + input_with_anomaly <- mock_input_big + input_with_anomaly$calculate_anomaly_scores <- TRUE + input_with_anomaly$run_order_file <- list(datapath = "run_order.csv") + + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + stub(getData, "showNotification", function(...) NULL) + stub(getData, "MSstatsBig::bigSpectronauttoMSstatsFormat", + mock_big_spec_converter) + captured_args <- NULL + stub(getData, "dplyr::collect", function(x) { + captured_args <<- x + mock_df + }) + # Skip the post-collect scoring call for this test — it's + # exercised separately below. + stub(getData, "data.table::fread", + data.frame(Run = "run1", Order = 1L)) + stub(getData, "MSstatsConvert::MSstatsAnomalyScores", + function(...) mock_df) + + getData(input_with_anomaly) + + expect_true(isTRUE(captured_args$calculateAnomalyScores)) + # Raw Spectronaut export names — the converter applies + # .standardizeColnames internally on the way out. + expect_equal(captured_args$anomalyModelFeatures, + c("FG.ShapeQualityScore (MS2)", + "FG.ShapeQualityScore (MS1)", + "EG.DeltaRT")) + # The big-file converter itself does NOT take a runOrder arg — + # that's consumed by the separate MSstatsAnomalyScores step + # post-collect (covered in the next test). + expect_null(captured_args$runOrder) + }) + + test_that("calls MSstatsConvert::MSstatsAnomalyScores after collect when calculate_anomaly_scores && run_order_file are set", { + input_with_full_anomaly <- mock_input_big + input_with_full_anomaly$calculate_anomaly_scores <- TRUE + input_with_full_anomaly$run_order_file <- list(datapath = "run_order.csv") + + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + stub(getData, "showNotification", function(...) NULL) + stub(getData, "MSstatsBig::bigSpectronauttoMSstatsFormat", + mock_arrow_obj) + stub(getData, "dplyr::collect", mock_df) + + run_order_df <- data.frame(Run = c("run1", "run2"), + Order = c(1L, 2L), + stringsAsFactors = FALSE) + stub(getData, "data.table::fread", run_order_df) + + captured_scoring_args <- NULL + stub(getData, "MSstatsConvert::MSstatsAnomalyScores", + function(...) { + captured_scoring_args <<- list(...) + mock_df + }) + + getData(input_with_full_anomaly) + + expect_false(is.null(captured_scoring_args)) + expect_equal(captured_scoring_args$input, mock_df) + # Standardized column names — the in-memory data after collect + # has had .standardizeColnames applied during the converter + # step, so MSstatsAnomalyScores must look for these names. + expect_equal(captured_scoring_args$quality_metrics, + c("FGShapeQualityScore(MS2)", + "FGShapeQualityScore(MS1)", + "EGDeltaRT")) + expect_equal(captured_scoring_args$temporal_direction, + c("mean_decrease", + "mean_decrease", + "dispersion_increase")) + expect_equal(captured_scoring_args$run_order, run_order_df) + expect_equal(captured_scoring_args$n_trees, 100) + expect_equal(captured_scoring_args$max_depth, "auto") + expect_equal(captured_scoring_args$cores, 1) + }) + + test_that("fails fast when calculate_anomaly_scores is TRUE but run_order_file is missing", { + input_no_runorder <- mock_input_big + input_no_runorder$calculate_anomaly_scores <- TRUE + input_no_runorder$run_order_file <- NULL + + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + stub(getData, "showNotification", + function(msg, ...) expect_match(msg, "Run Order CSV")) + # The converter should never run; if it does, fail the test. + stub(getData, "shinybusy::update_modal_spinner", + function(...) stop("converter step reached despite missing run order")) + + res <- getData(input_no_runorder) + expect_null(res) + }) + + test_that("passes intensity to converter when spec_intensity_col is set", { + input_with_intensity <- mock_input_big + input_with_intensity$spec_intensity_col <- "FG.MS1Quantity" + + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + stub(getData, "showNotification", function(...) NULL) + stub(getData, "MSstatsBig::bigSpectronauttoMSstatsFormat", + mock_big_spec_converter) + captured_args <- NULL + stub(getData, "dplyr::collect", function(x) { + captured_args <<- x + mock_df + }) + + getData(input_with_intensity) + + expect_equal(captured_args$intensity, "FG.MS1Quantity") + }) + + test_that("omits annotation + anomaly args when neither is supplied", { + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + stub(getData, "showNotification", function(...) NULL) + stub(getData, "MSstatsBig::bigSpectronauttoMSstatsFormat", + mock_big_spec_converter) + captured_args <- NULL + stub(getData, "dplyr::collect", function(x) { + captured_args <<- x + mock_df + }) + + getData(mock_input_big) + + expect_null(captured_args$annotation) + expect_null(captured_args$calculateAnomalyScores) + expect_null(captured_args$anomalyModelFeatures) + }) }) # ============================================================================