npm-run-scripts 1.0.2

Fast interactive TUI for running npm scripts
Documentation
//! Test fixtures and helper functions for integration tests.

use std::fs;
use tempfile::TempDir;

/// Lock file types for testing runner detection.
#[derive(Debug, Clone, Copy)]
pub enum LockfileType {
    Npm,
    Yarn,
    Pnpm,
    Bun,
}

impl LockfileType {
    /// Get the filename for this lock file type.
    pub fn filename(&self) -> &'static str {
        match self {
            LockfileType::Npm => "package-lock.json",
            LockfileType::Yarn => "yarn.lock",
            LockfileType::Pnpm => "pnpm-lock.yaml",
            LockfileType::Bun => "bun.lockb",
        }
    }

    /// Get sample content for this lock file type.
    pub fn sample_content(&self) -> &'static str {
        match self {
            LockfileType::Npm => r#"{"lockfileVersion": 3, "packages": {}}"#,
            LockfileType::Yarn => "# THIS IS AN AUTOGENERATED FILE\n__metadata:\n  version: 6\n",
            LockfileType::Pnpm => "lockfileVersion: '9.0'\n",
            LockfileType::Bun => "binary lockfile content",
        }
    }
}

/// Create a temporary project directory with the specified scripts.
///
/// # Arguments
///
/// * `scripts` - A slice of (name, command) tuples
///
/// # Returns
///
/// A `TempDir` that will be cleaned up when dropped.
pub fn create_project(scripts: &[(&str, &str)]) -> TempDir {
    let temp = TempDir::new().expect("Failed to create temp directory");

    let scripts_json = scripts
        .iter()
        .map(|(name, cmd)| format!(r#"    "{}": "{}""#, name, cmd))
        .collect::<Vec<_>>()
        .join(",\n");

    let package_json = format!(
        r#"{{
  "name": "test-project",
  "version": "1.0.0",
  "scripts": {{
{}
  }}
}}"#,
        scripts_json
    );

    fs::write(temp.path().join("package.json"), package_json)
        .expect("Failed to write package.json");

    temp
}

/// Create a temporary project with scripts and descriptions.
///
/// # Arguments
///
/// * `scripts` - A slice of (name, command, description) tuples
pub fn create_project_with_descriptions(scripts: &[(&str, &str, &str)]) -> TempDir {
    let temp = TempDir::new().expect("Failed to create temp directory");

    let scripts_json = scripts
        .iter()
        .map(|(name, cmd, _)| format!(r#"    "{}": "{}""#, name, cmd))
        .collect::<Vec<_>>()
        .join(",\n");

    let scripts_info_json = scripts
        .iter()
        .filter(|(_, _, desc)| !desc.is_empty())
        .map(|(name, _, desc)| format!(r#"    "{}": "{}""#, name, desc))
        .collect::<Vec<_>>()
        .join(",\n");

    let package_json = format!(
        r#"{{
  "name": "test-project",
  "version": "1.0.0",
  "scripts": {{
{}
  }},
  "scripts-info": {{
{}
  }}
}}"#,
        scripts_json, scripts_info_json
    );

    fs::write(temp.path().join("package.json"), package_json)
        .expect("Failed to write package.json");

    temp
}

/// Create a temporary project with a config file.
///
/// # Arguments
///
/// * `scripts` - A slice of (name, command) tuples
/// * `config_toml` - TOML configuration content
pub fn create_project_with_config(scripts: &[(&str, &str)], config_toml: &str) -> TempDir {
    let temp = create_project(scripts);
    fs::write(temp.path().join(".nrsrc.toml"), config_toml).expect("Failed to write config");
    temp
}

/// Create a temporary project with a specific lockfile.
///
/// # Arguments
///
/// * `scripts` - A slice of (name, command) tuples
/// * `lockfile_type` - The type of lock file to create
pub fn create_project_with_lockfile(
    scripts: &[(&str, &str)],
    lockfile_type: LockfileType,
) -> TempDir {
    let temp = create_project(scripts);
    fs::write(
        temp.path().join(lockfile_type.filename()),
        lockfile_type.sample_content(),
    )
    .expect("Failed to write lockfile");
    temp
}

/// Create a temporary project with a packageManager field.
///
/// # Arguments
///
/// * `scripts` - A slice of (name, command) tuples
/// * `package_manager` - The packageManager field value (e.g., "pnpm@8.0.0")
pub fn create_project_with_package_manager(
    scripts: &[(&str, &str)],
    package_manager: &str,
) -> TempDir {
    let temp = TempDir::new().expect("Failed to create temp directory");

    let scripts_json = scripts
        .iter()
        .map(|(name, cmd)| format!(r#"    "{}": "{}""#, name, cmd))
        .collect::<Vec<_>>()
        .join(",\n");

    let package_json = format!(
        r#"{{
  "name": "test-project",
  "version": "1.0.0",
  "packageManager": "{}",
  "scripts": {{
{}
  }}
}}"#,
        package_manager, scripts_json
    );

    fs::write(temp.path().join("package.json"), package_json)
        .expect("Failed to write package.json");

    temp
}

/// Create a temporary project with both packageManager and lockfile.
pub fn create_project_with_pm_and_lockfile(
    scripts: &[(&str, &str)],
    package_manager: &str,
    lockfile_type: LockfileType,
) -> TempDir {
    let temp = create_project_with_package_manager(scripts, package_manager);
    fs::write(
        temp.path().join(lockfile_type.filename()),
        lockfile_type.sample_content(),
    )
    .expect("Failed to write lockfile");
    temp
}

/// Create an empty project (no scripts).
pub fn create_empty_project() -> TempDir {
    let temp = TempDir::new().expect("Failed to create temp directory");

    let package_json = r#"{
  "name": "empty-project",
  "version": "1.0.0",
  "scripts": {}
}"#;

    fs::write(temp.path().join("package.json"), package_json)
        .expect("Failed to write package.json");

    temp
}

/// Create a project with no scripts field at all.
pub fn create_project_no_scripts() -> TempDir {
    let temp = TempDir::new().expect("Failed to create temp directory");

    let package_json = r#"{
  "name": "no-scripts-project",
  "version": "1.0.0"
}"#;

    fs::write(temp.path().join("package.json"), package_json)
        .expect("Failed to write package.json");

    temp
}

/// Create a project with invalid package.json.
pub fn create_project_invalid_json() -> TempDir {
    let temp = TempDir::new().expect("Failed to create temp directory");

    fs::write(temp.path().join("package.json"), "{ invalid json }")
        .expect("Failed to write package.json");

    temp
}

/// Create a project with lifecycle scripts.
pub fn create_project_with_lifecycle_scripts() -> TempDir {
    create_project(&[
        ("dev", "vite"),
        ("build", "vite build"),
        ("test", "vitest"),
        ("preinstall", "echo preinstall"),
        ("postinstall", "husky install"),
        ("prepare", "husky"),
        ("prepublishOnly", "npm test"),
    ])
}

/// Create a large project with many scripts.
pub fn create_large_project(script_count: usize) -> TempDir {
    let scripts: Vec<(String, String)> = (0..script_count)
        .map(|i| (format!("script_{}", i), format!("echo Script {}", i)))
        .collect();

    let scripts_ref: Vec<(&str, &str)> = scripts
        .iter()
        .map(|(name, cmd)| (name.as_str(), cmd.as_str()))
        .collect();

    create_project(&scripts_ref)
}

/// Standard scripts for testing.
pub fn standard_scripts() -> Vec<(&'static str, &'static str)> {
    vec![
        ("dev", "vite"),
        ("build", "vite build"),
        ("test", "vitest"),
        ("lint", "eslint ."),
        ("format", "prettier --write ."),
    ]
}

/// Scripts with special characters.
pub fn scripts_with_special_chars() -> Vec<(&'static str, &'static str)> {
    vec![
        ("build:dev", "vite build --mode development"),
        ("build:prod", "vite build --mode production"),
        ("test:unit", "vitest --dir tests/unit"),
        ("test:e2e", "playwright test"),
        ("db:migrate", "prisma migrate dev"),
    ]
}

/// Unicode scripts for testing.
pub fn unicode_scripts() -> Vec<(&'static str, &'static str)> {
    vec![
        ("build", "echo 构建"),
        ("test", "echo テスト"),
        ("lint", "echo проверка"),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_project() {
        let temp = create_project(&[("dev", "vite"), ("build", "vite build")]);
        let package_json = temp.path().join("package.json");
        assert!(package_json.exists());

        let content = fs::read_to_string(package_json).unwrap();
        assert!(content.contains("\"dev\": \"vite\""));
        assert!(content.contains("\"build\": \"vite build\""));
    }

    #[test]
    fn test_create_project_with_lockfile() {
        let temp = create_project_with_lockfile(&[("dev", "vite")], LockfileType::Pnpm);
        assert!(temp.path().join("package.json").exists());
        assert!(temp.path().join("pnpm-lock.yaml").exists());
    }

    #[test]
    fn test_create_project_with_config() {
        let config = r#"
[appearance]
icons = false
"#;
        let temp = create_project_with_config(&[("dev", "vite")], config);
        assert!(temp.path().join("package.json").exists());
        assert!(temp.path().join(".nrsrc.toml").exists());
    }

    #[test]
    fn test_lockfile_types() {
        assert_eq!(LockfileType::Npm.filename(), "package-lock.json");
        assert_eq!(LockfileType::Yarn.filename(), "yarn.lock");
        assert_eq!(LockfileType::Pnpm.filename(), "pnpm-lock.yaml");
        assert_eq!(LockfileType::Bun.filename(), "bun.lockb");
    }
}