diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 11cd7e6ab387b848c948b030e319198340ef66a1..862db2e8601d1574ca0995297a017dc7cd03e192 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -7,5 +7,8 @@
 				"REditorSupport.r"
 			]
 		}
-	}
+	},
+	// Use 'postCreateCommand' to run commands after the container is created.
+	"postCreateCommand": "R -q -e 'install.packages(\"languageserver\");remotes::install_deps(dep = TRUE)'",
+	"postStartCommand": "R -q -e 'devtools::install()'"
 }
diff --git a/.vignettechunkignore b/.vignettechunkignore
new file mode 100644
index 0000000000000000000000000000000000000000..fc31c8f750cc9f4cfa1f37584f0fec48adbdaa09
--- /dev/null
+++ b/.vignettechunkignore
@@ -0,0 +1,7 @@
+# This file is used by the script tests/testthat/test-vignettes which test all
+# chunks including those with `eval=FALSE`
+# It serves to ignore chunks that should not be tested anyway
+# Format: `vignette file name`[space]`id of the chunk`
+V02.1_param_optim.Rmd hydroPSO1
+V02.1_param_optim.Rmd hydroPSO2
+V02.1_param_optim.Rmd resGLOB
diff --git a/tests/testthat/helper_vignettes.R b/tests/testthat/helper_vignettes.R
index 98951fef380002bc1243e705ceaf2ac5c5d124a1..ace7650cb7510eaa7117e2a654e727612e2af388 100644
--- a/tests/testthat/helper_vignettes.R
+++ b/tests/testthat/helper_vignettes.R
@@ -5,7 +5,8 @@
 #' @param force.eval Force execution of chunks with parameter eval=FALSE
 RunRmdChunks <- function(fileRmd,
                          tmpFolder = "../tmp",
-                         force.eval = TRUE) {
+                         force.eval = TRUE,
+                         chunkIgnore = getChunkIgnore()) {
   dir.create(tmpFolder, showWarnings = FALSE)
   output <- file.path(tmpFolder,
                       gsub("\\.Rmd", "\\.R", basename(fileRmd), ignore.case = TRUE))
@@ -13,7 +14,13 @@ RunRmdChunks <- function(fileRmd,
   sTxt <- readLines(output)
   if (force.eval) {
     sectionLines <- grep("^## ----", sTxt)
-    chunksEvalStart <- grep("^## ----.*eval=F", sTxt)
+    chunkIgnore <- chunkIgnore[[basename(fileRmd)]]
+    if (!is.null(chunkIgnore)) {
+      regexChunk <- sprintf("(?!(%s))", paste(chunkIgnore, collapse = "|"))
+    } else {
+      regexChunk <- ""
+    }
+    chunksEvalStart <- grep(paste0("^## ----", regexChunk, ".*eval=F"), sTxt, ignore.case=TRUE, perl = TRUE)
     if (length(chunksEvalStart) > 0) {
       if (sectionLines[length(sectionLines)] == chunksEvalStart[length(chunksEvalStart)]) {
         lastEvalStart <- length(chunksEvalStart) - 1
@@ -72,13 +79,22 @@ RunVignetteChunks <- function(vignette,
                               force.eval = TRUE) {
   if (file.exists(sprintf("../../vignettes/%s.Rmd", vignette))) {
     # testthat context in development environnement
-    RunRmdChunks(sprintf("../../vignettes/%s.Rmd", vignette), tmpFolder, force.eval)
+    RunRmdChunks(sprintf("../../vignettes/%s.Rmd", vignette),
+                 tmpFolder = tmpFolder,
+                 force.eval =force.eval,
+                 chunkIgnore = getChunkIgnore("../../.vignettechunkignore"))
   } else if (file.exists(sprintf("vignettes/%s.Rmd", vignette))) {
     # context in direct run in development environnement
-    RunRmdChunks(sprintf("vignettes/%s.Rmd", vignette), tmpFolder, force.eval)
+    RunRmdChunks(sprintf("vignettes/%s.Rmd", vignette),
+                 tmpFolder = tmpFolder,
+                 force.eval =force.eval,
+                 chunkIgnore = getChunkIgnore(".vignettechunkignore"))
   } else {
     # R CMD check context in package environnement
-    RunRmdChunks(system.file(sprintf("doc/%s.Rmd", vignette), package = "airGR"), tmpFolder, force.eval)
+    RunRmdChunks(system.file(sprintf("doc/%s.Rmd", vignette), package = "airGR"),
+                 tmpFolder = tmpFolder,
+                 force.eval =force.eval,
+                 chunkIgnore = getChunkIgnore(".vignettechunkignore"))
   }
   return(TRUE)
 }
@@ -96,3 +112,26 @@ TestQmmQlsConversion <- function(BasinObs, BasinArea, tolerance = 1E-7) {
   notNA <- which(!is.na(BasinObs$Qmm))
   expect_equal(BasinObs$Qmm[notNA] * Conversion, BasinObs$Qls[notNA], tolerance = tolerance)
 }
+
+#' Read vignettechunkignore file
+#'
+#' @param chunkIgnoreFile path to the file
+#'
+#' @return [list] with one item by vignette containing the chunk id to ignore
+#'
+getChunkIgnore <- function(chunkIgnoreFile = "../../.vignettechunkignore") {
+  if (file.exists(chunkIgnoreFile)) {
+    message(".vignettechunkignore file found")
+    chunkIgnore <- read.table(file = chunkIgnoreFile,
+                              sep = " ", header = FALSE,
+                              col.names = c("vignette", "chunk"),
+                              stringsAsFactors = FALSE)
+    chunkIgnore <- lapply(setNames(nm = unique(chunkIgnore$vignette)), function(x) {
+      chunkIgnore$chunk[chunkIgnore$vignette == x]
+    })
+  } else {
+    message("No .vignettechunkignore file found")
+    chunkIgnore <- list()
+  }
+  return(chunkIgnore)
+}
diff --git a/vignettes/V02.1_param_optim.Rmd b/vignettes/V02.1_param_optim.Rmd
index 6c43ac8583b48f02e5ccf2766e80abdad8a8a422..d0185b731b451d39127aa55837995cf0baa73cdd 100644
--- a/vignettes/V02.1_param_optim.Rmd
+++ b/vignettes/V02.1_param_optim.Rmd
@@ -10,10 +10,10 @@ vignette: >
 
 
 
-```{r, warning=FALSE, include=FALSE, fig.keep='none', results='hide'}
+```{r setup, warning=FALSE, include=FALSE, fig.keep='none', results='hide'}
 library(airGR)
 library(DEoptim)
-# library(hydroPSO) # Needs R version >= 3.6 or latticeExtra <= 0.6-28 on R 3.5. Archived on 2023-10-16 as requires archived packages 'hydroTSM' and 'hydroGOF'. 
+# library(hydroPSO) # Needs R version >= 3.6 or latticeExtra <= 0.6-28 on R 3.5. Archived on 2023-10-16 as requires archived packages 'hydroTSM' and 'hydroGOF'.
 library(Rmalschains)
 library(caRamel)
 library(ggplot2)
@@ -41,13 +41,13 @@ Please note that the calibration period is defined in the `CreateRunOptions()` f
 <!-- example("Calibration_Michel", echo = FALSE, ask = FALSE) -->
 <!-- ``` -->
 
-```{r, echo=TRUE, eval=FALSE}
+```{r Calibration_Michel, echo=TRUE, eval=FALSE}
 example("Calibration_Michel")
 ```
 
 In order for the `RunModel_*()` functions to run faster during the parameter estimation process, it is recommended that the outputs contain only the simulated flows (see the `Outputs_Sim` argument in the `CreateRunOptions()` help page).
 
-```{r, results='hide', eval=FALSE}
+```{r RunOptions, results='hide', eval=FALSE}
 RunOptions <- airGR::CreateRunOptions(FUN_MOD = RunModel_GR4J, InputsModel = InputsModel,
                                       IndPeriod_Run = Ind_Run,
                                       Outputs_Sim = "Qsim")
@@ -66,7 +66,7 @@ Here we choose to minimize the root mean square error.
 
 The change of the repository from the "real" parameter space to a "transformed" space ensures homogeneity of displacement in the different dimensions of the parameter space during the step-by-step procedure of the calibration algorithm of the model.
 
-```{r, warning=FALSE, results='hide', eval=FALSE}
+```{r OptimGR4J, warning=FALSE, results='hide', eval=FALSE}
 OptimGR4J <- function(ParamOptim) {
   ## Transformation of the parameter set to real space
   RawParamOptim <- airGR::TransfoParam_GR4J(ParamIn = ParamOptim,
@@ -86,7 +86,7 @@ OptimGR4J <- function(ParamOptim) {
 
 In addition, we need to define the lower and upper bounds of the four **GR4J** parameters in the transformed parameter space:
 
-```{r, warning=FALSE, results='hide', eval=FALSE}
+```{r boundsGR4J, warning=FALSE, results='hide', eval=FALSE}
 lowerGR4J <- rep(-9.99, times = 4)
 upperGR4J <- rep(+9.99, times = 4)
 ```
@@ -97,7 +97,7 @@ upperGR4J <- rep(+9.99, times = 4)
 
 We start with a local optimization strategy by using the PORT routines (using the `nlminb()` of the `stats` package) and by setting a starting point in the transformed parameter space:
 
-```{r, warning=FALSE, results='hide', eval=FALSE}
+```{r local1, warning=FALSE, results='hide', eval=FALSE}
 startGR4J <- c(4.1, 3.9, -0.9, -8.7)
 optPORT <- stats::nlminb(start = startGR4J,
                          objective = OptimGR4J,
@@ -111,7 +111,7 @@ We can also try a multi-start approach to test the consistency of the local opti
 Here we use the same grid used for the filtering step of the Michel's calibration strategy (`Calibration_Michel()` function).
 For each starting point, a local optimization is performed.
 
-```{r, warning=FALSE, results='hide', eval=FALSE}
+```{r local2, warning=FALSE, results='hide', eval=FALSE}
 startGR4JDistrib <- TransfoParam_GR4J(ParamIn = CalibOptions$StartParamDistrib,
                                       Direction = "RT")
 startGR4J <- expand.grid(data.frame(startGR4JDistrib))
@@ -126,7 +126,7 @@ listOptPORT <- apply(startGR4J, MARGIN = 1, FUN = optPORT_)
 
 We can then extract the best parameter sets and the value of the performance criteria:
 
-```{r, warning=FALSE, results='hide', eval=FALSE}
+```{r local3, warning=FALSE, results='hide', eval=FALSE}
 parPORT <- t(sapply(listOptPORT, function(x) x$par))
 objPORT <- sapply(listOptPORT, function(x) x$objective)
 resPORT <- data.frame(parPORT, RMSE = objPORT)
@@ -134,7 +134,7 @@ resPORT <- data.frame(parPORT, RMSE = objPORT)
 
 As can be seen below, the optimum performance criterion values (column *objective*) can differ from the global optimum value in many cases, resulting in various parameter sets.
 
-```{r, warning=FALSE}
+```{r local4, warning=FALSE}
 summary(resPORT)
 ```
 
@@ -154,7 +154,7 @@ Here we use the following R implementation of some popular strategies:
 
 ## Differential Evolution
 
-```{r, warning=FALSE, results='hide', eval=FALSE}
+```{r optDE, warning=FALSE, results='hide', eval=FALSE}
 optDE <- DEoptim::DEoptim(fn = OptimGR4J,
                           lower = lowerGR4J, upper = upperGR4J,
                           control = DEoptim::DEoptim.control(NP = 40, trace = 10))
@@ -163,15 +163,15 @@ optDE <- DEoptim::DEoptim(fn = OptimGR4J,
 
 ## Particle Swarm
 
-```{r, warning=FALSE, results='hide', message=FALSE, eval=FALSE}
+```{r hydroPSO1, warning=FALSE, results='hide', message=FALSE, eval=FALSE}
 # to install the package temporary removed from CRAN
 # Rtools needed (windows : https://cran.r-project.org/bin/windows/Rtools/)
-# install.packages("https://cran.r-project.org/src/contrib/Archive/hydroPSO/hydroPSO_0.5-1.tar.gz", 
+# install.packages("https://cran.r-project.org/src/contrib/Archive/hydroPSO/hydroPSO_0.5-1.tar.gz",
 #                  repos = NULL, type = "source", dependencies = TRUE)
 ```
 
 
-```{r, warning=FALSE, results='hide', message=FALSE, eval=FALSE}
+```{r hydroPSO2, warning=FALSE, results='hide', message=FALSE, eval=FALSE}
 optPSO <- hydroPSO::hydroPSO(fn = OptimGR4J,
                              lower = lowerGR4J, upper = upperGR4J,
                              control = list(write2disk = FALSE, verbose = FALSE))
@@ -192,7 +192,7 @@ optMALS <- Rmalschains::malschains(fn = OptimGR4J,
 
 As it can be seen in the table below, the four additional optimization strategies tested lead to very close optima.
 
-```{r, warning=FALSE, echo=FALSE, eval=FALSE}
+```{r resGLOB, warning=FALSE, echo=FALSE, eval=FALSE}
 resGLOB <- data.frame(Algo = c("airGR", "PORT", "DE", "PSO", "MA-LS"),
                       round(rbind(
                         OutputsCalib$ParamFinalR,
@@ -223,7 +223,7 @@ First, the OptimGR4J function previously used is modified to return two values.
 ```{r, warning=FALSE, results='hide', eval=FALSE}
 InputsCrit_inv <- InputsCrit
 InputsCrit_inv$transfo <- "inv"
-  
+
 MOptimGR4J <- function(i) {
   if (algo == "caRamel") {
     ParamOptim <- x[i, ]
@@ -270,9 +270,9 @@ optMO <- caRamel::caRamel(nobj = 2,
 The algorithm returns parameter sets that describe the pareto front, illustrating the trade-off between overall good performance and good performance for low flow.
 
 ```{r, fig.width=6, fig.height=6, warning=FALSE}
-ggplot() + 
+ggplot() +
   geom_point(aes(optMO$objectives[, 1], optMO$objectives[, 2])) +
-  coord_equal(xlim = c(0.4, 0.9), ylim = c(0.4, 0.9)) + 
+  coord_equal(xlim = c(0.4, 0.9), ylim = c(0.4, 0.9)) +
   xlab("KGE") + ylab("KGE [1/Q]") +
   theme_bw()
 ```