From 4b874232e17ac221af36fa97610fce1e2f7bdfcc Mon Sep 17 00:00:00 2001 From: Pablo Tiscornia Date: Mon, 4 May 2026 23:25:11 -0600 Subject: [PATCH] test: Sprint test-1 batch 3 (+70 tests, total 149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cobertura de funciones puras restantes del Sprint test-1: - arma_matriz_transicion (R/utils_analisis.R): 6 tests para matriz NxN (estructura, suma 100% por fila, orden de etiquetas, manejo de categorías ausentes, soporte CAT_OCUP). - build_tasas_historico (ETL/99-functions.R): 8 tests para output trimestral/anual, invariantes (persistencia + salida = 100, tasas en [0, 100]), filtro desde_panel, vars_extra. - regenerar_calidad_panel (ETL/99-functions.R): 6 tests con with_tempdir() para schema, formato de periodos, idempotencia, % en rango, n_panel <= n_t0. - formato_delta (R/utils_analisis.R): 4 tests para flecha + signo + 'sin comparación' en NA/NULL. - sankey_label_legible (R/utils_analisis.R): 5 tests para mapeo de ESTADO/CAT_OCUP/formalidad, vectorización, fallback. - sankey_nodes_orden (R/utils_analisis.R): 3 tests para estructura de columna y orden no-alfabético. Quedan fuera del Sprint test-1: armo_base_panel modo legacy (mejor cubrirlo en Sprint test-2 junto con runtime mode usando testServer). Suite completa: 149 tests PASS · 0 fail · 1m local. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 11 ++ ROADMAP.md | 16 +- tests/testthat/test-arma_matriz_transicion.R | 156 ++++++++++++++++++ tests/testthat/test-build_tasas_historico.R | 153 +++++++++++++++++ tests/testthat/test-regenerar_calidad_panel.R | 147 +++++++++++++++++ tests/testthat/test-utils_pequenos.R | 100 +++++++++++ 6 files changed, 578 insertions(+), 5 deletions(-) create mode 100644 tests/testthat/test-arma_matriz_transicion.R create mode 100644 tests/testthat/test-build_tasas_historico.R create mode 100644 tests/testthat/test-regenerar_calidad_panel.R create mode 100644 tests/testthat/test-utils_pequenos.R diff --git a/CHANGELOG.md b/CHANGELOG.md index a78c7aa..40181f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ versionado [SemVer](https://semver.org/lang/es/) adaptado a app web: --- +## [Unreleased] + +### Added + +- Sprint test-1 batch 3: tests para `arma_matriz_transicion`, + `build_tasas_historico`, `regenerar_calidad_panel`, `formato_delta`, + `sankey_label_legible`, `sankey_nodes_orden`. Suite de testthat pasa + de 79 a **149 tests verde**. Cobertura aproximada del Sprint test-1 + cerrada salvo `armo_base_panel` legacy (movido a Sprint test-2 para + combinarlo con cobertura de modo runtime via `testServer`). + ## [0.9.0] · 2026-05-04 Cierra Sprint A (#44 Tipo de dúo end-to-end). El toggle Interanual diff --git a/ROADMAP.md b/ROADMAP.md index 09b3747..387b23f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -21,11 +21,17 @@ Sprint A). Ver [CHANGELOG.md](CHANGELOG.md) para el detalle. - [x] Setup `tests/testthat/` + runner + helper-fixtures - [x] Fixture sintética `panel_mock.rds` (100 individuos × 3 ondas) -- [x] Tests: `agrega_vars_derivadas`, `armo_tabla_sankey`, - `duos_disponibles_por_anio`, `duo_label` → **42 tests PASS** -- [ ] Tests pendientes: `arma_tasas_destacadas`, - `regenerar_panel_historico`, `armo_base_panel` modo legacy -- [ ] CI: GitHub Actions `tests-unit.yml` +- [x] Tests batch 1: `agrega_vars_derivadas`, `armo_tabla_sankey`, + `duos_disponibles_por_anio`, `duo_label` → 42 tests +- [x] Tests batch 2: `arma_tasas_destacadas`, `regenerar_panel_historico`, + `tests/testthat.R` aislado de `00-libraries.R` → 79 tests +- [x] Tests batch 3: `arma_matriz_transicion`, `build_tasas_historico`, + `regenerar_calidad_panel`, `formato_delta`, `sankey_label_legible`, + `sankey_nodes_orden` → **149 tests PASS** +- [x] CI: GitHub Actions `tests-unit.yml` ejecuta en cada push a master/staging +- [ ] Pendiente para Sprint test-2: `armo_base_panel` modo legacy + (requiere `eph::organize_panels()` real, conviene cubrir junto + con runtime mode usando `testServer`). ### Sprint test-2 · Server logic con testServer() (~3-4 hs) diff --git a/tests/testthat/test-arma_matriz_transicion.R b/tests/testthat/test-arma_matriz_transicion.R new file mode 100644 index 0000000..25a1ccd --- /dev/null +++ b/tests/testthat/test-arma_matriz_transicion.R @@ -0,0 +1,156 @@ +### Tests de arma_matriz_transicion() (en R/utils_analisis.R). +### +### Construye una matriz NxN de transición (porcentaje sobre el total t0 +### de cada fila) usando preparo_base() con periodo_base = "t_anterior". +### +### Output: tibble con `from` como primera col y una col por categoría +### destino. Valores en porcentaje, deberían sumar ~100 por fila (con +### tolerancia por redondeo a 1 decimal). + +test_that("arma_matriz_transicion: estructura del output", { + ### Panel mínimo controlado: + ### 3 personas Ocupado_t0 → 2 Ocupado_t1 + 1 Desocupado_t1 + ### 2 personas Desocupado_t0 → 1 Ocupado_t1 + 1 Inactivo_t1 + df_panel <- tibble::tibble( + ESTADO = c(1L, 1L, 1L, 2L, 2L), + ESTADO_t1 = c(1L, 1L, 2L, 1L, 3L), + PONDERA = c(500, 500, 500, 500, 500), + PONDERA_t1 = c(500, 500, 500, 500, 500) + ) + + matriz <- arma_matriz_transicion( + df_panel = df_panel, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar") + ) + + expect_s3_class(matriz, "tbl_df") + expect_true("from" %in% names(matriz)) + ### Etiquetas legibles aplicadas: "Ocupado" → "Ocupados", etc. + expect_true(all(c("Ocupados", "Desocupados", "Inactivos", + "Trab. familiares") %in% names(matriz))) +}) + + +test_that("arma_matriz_transicion: filas suman ~100% (tolerancia redondeo)", { + df_panel <- tibble::tibble( + ESTADO = c(1L, 1L, 1L, 2L, 2L), + ESTADO_t1 = c(1L, 1L, 2L, 1L, 3L), + PONDERA = c(500, 500, 500, 500, 500), + PONDERA_t1 = c(500, 500, 500, 500, 500) + ) + + matriz <- arma_matriz_transicion( + df_panel = df_panel, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar") + ) + + ### Sumar todas las cols numéricas por fila. Tolerancia ±0.5 pp por + ### redondeo a 1 decimal en preparo_base. + cols_num <- setdiff(names(matriz), "from") + sumas <- rowSums(matriz[, cols_num]) + expect_true(all(abs(sumas - 100) < 1)) +}) + + +test_that("arma_matriz_transicion: valores específicos para caso controlado", { + ### Panel: + ### 2 Ocupado_t0 → Ocupado_t1 (PONDERA 500 c/u → 1000) + ### 1 Ocupado_t0 → Desocupado_t1 (PONDERA 500) + ### Total Ocupado_t0 = 1500 → 1000/1500 = 66.7%, 500/1500 = 33.3% + df_panel <- tibble::tibble( + ESTADO = c(1L, 1L, 1L), + ESTADO_t1 = c(1L, 1L, 2L), + PONDERA = c(500, 500, 500), + PONDERA_t1 = c(500, 500, 500) + ) + + matriz <- arma_matriz_transicion( + df_panel = df_panel, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar") + ) + + fila_ocup <- matriz |> dplyr::filter(from == "Ocupados") + expect_equal(fila_ocup$Ocupados, 66.7) + expect_equal(fila_ocup$Desocupados, 33.3) + expect_equal(fila_ocup$Inactivos, 0) +}) + + +test_that("arma_matriz_transicion: orden de filas y cols sigue 'etiquetas' (no alfabético)", { + ### Panel con las 4 categorías presentes para que aparezcan las 4 filas. + df_panel <- tibble::tibble( + ESTADO = c(1L, 2L, 3L, 4L), + ESTADO_t1 = c(1L, 2L, 3L, 4L), + PONDERA = c(500, 500, 500, 500), + PONDERA_t1 = c(500, 500, 500, 500) + ) + + ### Etiquetas en orden NO alfabético: Inactivo, Ocupado, Desocupado, ... + matriz <- arma_matriz_transicion( + df_panel = df_panel, + var = "ESTADO", + etiquetas = c("Inactivo", "Ocupado", "Desocupado", "Trab_familiar") + ) + + ### Filas: el factor con levels = remap(etiquetas) define el orden, + ### dplyr::arrange(from, to) lo respeta. + expect_equal(matriz$from, c("Inactivos", "Ocupados", "Desocupados", + "Trab. familiares")) + ### Cols (después de "from"): mismo orden. + cols <- setdiff(names(matriz), "from") + expect_equal(cols, c("Inactivos", "Ocupados", "Desocupados", + "Trab. familiares")) +}) + + +test_that("arma_matriz_transicion: funciona con CAT_OCUP", { + ### Mismo schema pero variable CAT_OCUP. Códigos 1=Patron, 2=Cuenta_propia, + ### 3=Asalariado, 4=TFSR. + df_panel <- tibble::tibble( + CAT_OCUP = c(3L, 3L, 2L), + CAT_OCUP_t1 = c(3L, 3L, 3L), + PONDERA = c(500, 500, 500), + PONDERA_t1 = c(500, 500, 500) + ) + + matriz <- arma_matriz_transicion( + df_panel = df_panel, + var = "CAT_OCUP", + etiquetas = c("Patron", "Cuenta_propia", "Asalariado", "TFSR") + ) + + ### Etiquetas legibles esperadas según el mapeo de la función. + expect_true(all(c("Patrones", "Cuenta propia", "Asalariados", + "Trab. familiares") %in% names(matriz))) +}) + + +test_that("arma_matriz_transicion: categoría sin presencia → no aparece como fila pero SÍ como columna", { + ### Sin Trab_familiar en el panel: la columna existe con 0s gracias a + ### names_expand = TRUE + factor con levels en `to`. Pero la fila NO + ### se genera (no hay registros que la tengan como `from`). + df_panel <- tibble::tibble( + ESTADO = c(1L, 2L, 3L), + ESTADO_t1 = c(1L, 2L, 3L), + PONDERA = c(500, 500, 500), + PONDERA_t1 = c(500, 500, 500) + ) + + matriz <- arma_matriz_transicion( + df_panel = df_panel, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar") + ) + + ### Columna "Trab. familiares" presente y todo 0 (no hay transiciones + ### hacia esa categoría en el panel). + expect_true("Trab. familiares" %in% names(matriz)) + expect_true(all(matriz[["Trab. familiares"]] == 0)) + + ### Pero la fila NO se genera (no hay nadie con ESTADO=4 en t0). + expect_false("Trab. familiares" %in% matriz$from) + expect_equal(nrow(matriz), 3) +}) diff --git a/tests/testthat/test-build_tasas_historico.R b/tests/testthat/test-build_tasas_historico.R new file mode 100644 index 0000000..65de4fd --- /dev/null +++ b/tests/testthat/test-build_tasas_historico.R @@ -0,0 +1,153 @@ +### Tests de build_tasas_historico() (en ETL/99-functions.R). +### +### Construye un tibble con (periodo, categoria, persistencia, salida, +### entrada) iterando sobre todos los duos válidos del microdato. NO +### escribe a disco — devuelve el tibble in-memory. +### +### Acepta `window`: "trimestral" (default) o "anual" (issue #46). + +test_that("build_tasas_historico trimestral: schema y filas > 0", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + out <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"), + window = "trimestral" + ) + + expect_s3_class(out, "tbl_df") + expect_true(all(c("periodo", "categoria", "persistencia", "salida", + "entrada") %in% names(out))) + expect_gt(nrow(out), 0) +}) + + +test_that("trimestral: periodos en formato 'YYYY_tA-tB'", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + out <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"), + window = "trimestral" + ) + + ### Fixture tiene 2024-T1, T2, T3 → 2 dúos consecutivos. + expect_true(all(out$periodo %in% c("2024_t1-t2", "2024_t2-t3"))) +}) + + +test_that("anual: periodos en formato 'YYYY_tN' (sin '-tM')", { + ### Construir microdato extendido a 2025-T1 para tener al menos un dúo + ### anual válido (2024-T1 → 2025-T1). + base <- load_panel_mock() + df_2025 <- base |> + dplyr::filter(TRIMESTRE == 1L) |> + dplyr::mutate(ANO4 = 2025L) + df_microdato <- dplyr::bind_rows(base, df_2025) |> agrega_vars_derivadas() + + out <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"), + window = "anual" + ) + + ### Solo dúo válido: 2024-T1 → 2025-T1, periodo "2024_t1". + expect_true(all(out$periodo == "2024_t1")) +}) + + +test_that("una fila por (periodo, categoria) y todas las etiquetas presentes", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + etiquetas <- c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar") + + out <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = etiquetas, + window = "trimestral" + ) + + ### 2 dúos x 4 categorías = hasta 8 filas. Puede haber menos si alguna + ### categoría no es computable en algún dúo, pero todas deberían estar + ### representadas al menos una vez en el panel mock. + expect_setequal(unique(out$categoria), etiquetas) +}) + + +test_that("invariante: persistencia + salida = 100 por fila", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + out <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"), + window = "trimestral" + ) + + ### Tolerancia ±0.2 pp por redondeo a 1 decimal en ambas tasas. + deltas <- abs(out$persistencia + out$salida - 100) + expect_true(all(deltas < 0.2)) +}) + + +test_that("invariante: tasas en rango [0, 100]", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + out <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"), + window = "trimestral" + ) + + expect_true(all(out$persistencia >= 0 & out$persistencia <= 100)) + expect_true(all(out$salida >= 0 & out$salida <= 100)) + expect_true(all(out$entrada >= 0 & out$entrada <= 100)) +}) + + +test_that("desde_panel filtra periodos previos", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + ### Sin filtro: 2 dúos (2024_t1-t2, 2024_t2-t3). + out_full <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"), + window = "trimestral" + ) + + ### Con desde_panel = "2024T2": solo 2024_t2-t3. + out_filt <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"), + desde_panel = "2024T2", + window = "trimestral" + ) + + expect_true(all(out_filt$periodo == "2024_t2-t3")) + expect_lt(nrow(out_filt), nrow(out_full)) +}) + + +test_that("vars_extra: pasa columnas extra al panel sin afectar el output schema", { + ### vars_extra se usa internamente en armo_base_panel para incluir cols + ### adicionales en el panel (ej: CH04, CH06 para validaciones), pero el + ### output de tasas no las expone. + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + out <- build_tasas_historico( + df_microdato = df_microdato, + var = "ESTADO", + etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"), + vars_extra = c("CH04", "CH06"), + window = "trimestral" + ) + + expect_setequal(names(out), c("periodo", "categoria", "persistencia", + "salida", "entrada")) +}) diff --git a/tests/testthat/test-regenerar_calidad_panel.R b/tests/testthat/test-regenerar_calidad_panel.R new file mode 100644 index 0000000..67ae2f2 --- /dev/null +++ b/tests/testthat/test-regenerar_calidad_panel.R @@ -0,0 +1,147 @@ +### Tests de regenerar_calidad_panel() (en ETL/99-functions.R). +### +### Función que regenera incrementalmente un CSV de métricas de calidad +### de panel (% encontrado, % inconsistencias por sexo/edad). Acepta +### `window` (issue #47): "trimestral" (default) o "anual". +### +### Estrategia: fixture sintético + with_tempdir() para que el CSV se +### escriba a un dir temporal sin tocar data_output/. + +test_that("regenerar_calidad_panel trimestral genera CSV con schema esperado", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + withr::with_tempdir({ + path <- file.path(getwd(), "test_calidad.csv") + + out <- regenerar_calidad_panel( + path_csv = path, + df_microdato = df_microdato, + window = "trimestral" + ) + + expect_true(file.exists(path)) + + df <- readr::read_csv(path, show_col_types = FALSE) + + cols_esperadas <- c( + "periodo", "anio_0", "trim_0", "anio_1", "trim_1", + "n_t0", "pondera_t0", "n_panel", "pondera_panel", + "n_inc_total", "n_inc_sexo", "n_inc_edad", + "pondera_inc_total", "pondera_inc_sexo", "pondera_inc_edad", + "pct_encontrado_n", "pct_encontrado_pondera", + "pct_inc_total", "pct_inc_sexo", "pct_inc_edad" + ) + expect_true(all(cols_esperadas %in% names(df))) + expect_gt(nrow(df), 0) + }) +}) + + +test_that("trimestral: periodos en formato 'YYYY_tA-tB'", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + withr::with_tempdir({ + path <- file.path(getwd(), "test_calidad.csv") + + regenerar_calidad_panel( + path_csv = path, + df_microdato = df_microdato, + window = "trimestral" + ) + + df <- readr::read_csv(path, show_col_types = FALSE) + expect_true(all(df$periodo %in% c("2024_t1-t2", "2024_t2-t3"))) + }) +}) + + +test_that("anual: periodos en formato 'YYYY_tN' (sin '-tM')", { + ### Microdato extendido a 2025 para tener un dúo anual válido. + base <- load_panel_mock() + df_2025 <- base |> + dplyr::filter(TRIMESTRE == 1L) |> + dplyr::mutate( + ANO4 = 2025L, + CH06 = CH06 + 1L # avanza 1 año (consistente con esperado para anual) + ) + df_microdato <- dplyr::bind_rows(base, df_2025) |> agrega_vars_derivadas() + + withr::with_tempdir({ + path <- file.path(getwd(), "test_calidad_anual.csv") + + regenerar_calidad_panel( + path_csv = path, + df_microdato = df_microdato, + window = "anual" + ) + + df <- readr::read_csv(path, show_col_types = FALSE) + expect_true(all(df$periodo == "2024_t1")) + }) +}) + + +test_that("idempotencia: segunda corrida no agrega filas duplicadas", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + withr::with_tempdir({ + path <- file.path(getwd(), "test_calidad.csv") + + regenerar_calidad_panel( + path_csv = path, + df_microdato = df_microdato, + window = "trimestral" + ) + n_primera <- nrow(readr::read_csv(path, show_col_types = FALSE)) + + regenerar_calidad_panel( + path_csv = path, + df_microdato = df_microdato, + window = "trimestral" + ) + n_segunda <- nrow(readr::read_csv(path, show_col_types = FALSE)) + + expect_equal(n_segunda, n_primera) + }) +}) + + +test_that("invariante: porcentajes en rango [0, 100]", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + withr::with_tempdir({ + path <- file.path(getwd(), "test_calidad.csv") + + regenerar_calidad_panel( + path_csv = path, + df_microdato = df_microdato, + window = "trimestral" + ) + + df <- readr::read_csv(path, show_col_types = FALSE) + expect_true(all(df$pct_encontrado_n >= 0 & df$pct_encontrado_n <= 100)) + expect_true(all(df$pct_encontrado_pondera >= 0 & df$pct_encontrado_pondera <= 100)) + expect_true(all(df$pct_inc_total >= 0 & df$pct_inc_total <= 100)) + expect_true(all(df$pct_inc_sexo >= 0 & df$pct_inc_sexo <= 100)) + expect_true(all(df$pct_inc_edad >= 0 & df$pct_inc_edad <= 100)) + }) +}) + + +test_that("invariante: n_panel <= n_t0 (encontrados nunca > totales)", { + df_microdato <- load_panel_mock() |> agrega_vars_derivadas() + + withr::with_tempdir({ + path <- file.path(getwd(), "test_calidad.csv") + + regenerar_calidad_panel( + path_csv = path, + df_microdato = df_microdato, + window = "trimestral" + ) + + df <- readr::read_csv(path, show_col_types = FALSE) + expect_true(all(df$n_panel <= df$n_t0)) + expect_true(all(df$pondera_panel <= df$pondera_t0)) + }) +}) diff --git a/tests/testthat/test-utils_pequenos.R b/tests/testthat/test-utils_pequenos.R new file mode 100644 index 0000000..cc8c1dc --- /dev/null +++ b/tests/testthat/test-utils_pequenos.R @@ -0,0 +1,100 @@ +### Tests de utilidades pequeñas en R/utils_analisis.R: +### - formato_delta(): formatea un delta en pp con flecha y signo. +### - sankey_label_legible(): convierte códigos técnicos en labels legibles. +### - sankey_nodes_orden(): construye lista de nodos para forzar orden vertical. + +### --- formato_delta() --------------------------------------------------- + +test_that("formato_delta: positivo > 0.05 muestra '↑ +X.X pp'", { + expect_equal(formato_delta(1.2), "↑ +1.2 pp") + expect_equal(formato_delta(0.1), "↑ +0.1 pp") + expect_equal(formato_delta(15.7), "↑ +15.7 pp") +}) + +test_that("formato_delta: negativo < -0.05 muestra '↓ -X.X pp'", { + expect_equal(formato_delta(-0.5), "↓ -0.5 pp") + expect_equal(formato_delta(-3.4), "↓ -3.4 pp") +}) + +test_that("formato_delta: cerca de 0 muestra '= 0.0 pp'", { + ### Umbral: |delta| <= 0.05 se considera sin cambio. + expect_equal(formato_delta(0), "= 0.0 pp") + expect_equal(formato_delta(0.04), "= 0.0 pp") + expect_equal(formato_delta(-0.03), "= -0.0 pp") +}) + +test_that("formato_delta: NA o NULL devuelve 'sin comparación'", { + expect_equal(formato_delta(NA), "sin comparación") + expect_equal(formato_delta(NA_real_), "sin comparación") + expect_equal(formato_delta(NULL), "sin comparación") + expect_equal(formato_delta(numeric(0)), "sin comparación") +}) + + +### --- sankey_label_legible() ------------------------------------------- + +test_that("sankey_label_legible: ESTADO codes mapean correctamente", { + expect_equal(sankey_label_legible("Ocupado_t0"), "Ocupados (t0)") + expect_equal(sankey_label_legible("Desocupado_t1"), "Desocupados (t1)") + expect_equal(sankey_label_legible("Inactivo_t0"), "Inactivos (t0)") + expect_equal(sankey_label_legible("Trab_familiar_t1"), "Trab. familiares (t1)") +}) + +test_that("sankey_label_legible: CAT_OCUP codes mapean correctamente", { + expect_equal(sankey_label_legible("Patron_t0"), "Patrones (t0)") + expect_equal(sankey_label_legible("Cuenta_propia_t1"), "Cuenta propia (t1)") + expect_equal(sankey_label_legible("Asalariado_t0"), "Asalariados (t0)") + expect_equal(sankey_label_legible("TFSR_t1"), "Trab. familiares (t1)") +}) + +test_that("sankey_label_legible: formalidad codes mapean correctamente", { + expect_equal(sankey_label_legible("Formal_t0"), "Formales (t0)") + expect_equal(sankey_label_legible("Informal_t1"), "Informales (t1)") +}) + +test_that("sankey_label_legible: vectorizado", { + codigos <- c("Ocupado_t0", "Desocupado_t1", "Inactivo_t0") + esperado <- c("Ocupados (t0)", "Desocupados (t1)", "Inactivos (t0)") + expect_equal(sankey_label_legible(codigos), esperado) +}) + +test_that("sankey_label_legible: código no mapeado usa fallback (mantiene base)", { + ### Si llega algo que no está en el mapeo, devuelve la base + sufijo + ### sin transformar (en lugar de NA). + expect_equal(sankey_label_legible("Otro_t0"), "Otro (t0)") +}) + + +### --- sankey_nodes_orden() -------------------------------------------- + +test_that("sankey_nodes_orden: 4 categorías → 8 nodos (4 t0 + 4 t1)", { + cats <- c("Ocupados", "Desocupados", "Inactivos", "Trab. familiares") + nodos <- sankey_nodes_orden(cats) + + expect_length(nodos, 8) + expect_true(all(vapply(nodos, is.list, logical(1)))) +}) + +test_that("sankey_nodes_orden: primeros 4 nodos son columna 0 (t0), siguientes 4 columna 1 (t1)", { + cats <- c("Ocupados", "Desocupados", "Inactivos", "Trab. familiares") + nodos <- sankey_nodes_orden(cats) + + ### Columnas: primeros 4 = 0, últimos 4 = 1. + cols <- vapply(nodos, function(n) n$column, numeric(1)) + expect_equal(cols, c(0, 0, 0, 0, 1, 1, 1, 1)) + + ### IDs: primeros 4 con sufijo (t0), últimos 4 con (t1), respetando + ### el orden del input (NO alfabético). + ids <- vapply(nodos, function(n) n$id, character(1)) + expect_equal(ids[1:4], paste0(cats, " (t0)")) + expect_equal(ids[5:8], paste0(cats, " (t1)")) +}) + +test_that("sankey_nodes_orden: respeta el orden del input (no alfabético)", { + ### Si el input no está ordenado alfabéticamente, la función debe + ### preservar ese orden (es la razón de existir de la fn). + cats <- c("Patrones", "Cuenta propia", "Asalariados", "Trab. familiares") + nodos <- sankey_nodes_orden(cats) + ids_t0 <- vapply(nodos[1:4], function(n) n$id, character(1)) + expect_equal(ids_t0, paste0(cats, " (t0)")) +})