Skip to main content

alef_e2e/codegen/typescript/
mod.rs

1//! TypeScript e2e test generator using vitest.
2
3mod assertions;
4mod config;
5mod json;
6mod test_file;
7mod visitors;
8
9use crate::config::E2eConfig;
10use crate::field_access::FieldResolver;
11use crate::fixture::FixtureGroup;
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use anyhow::Result;
15use std::path::PathBuf;
16
17use super::E2eCodegen;
18use config::{render_file_setup, render_global_setup, render_package_json, render_tsconfig, render_vitest_config};
19pub use test_file::render_test_file;
20use test_file::resolve_node_function_name;
21
22/// TypeScript e2e code generator.
23pub struct TypeScriptCodegen;
24
25impl E2eCodegen for TypeScriptCodegen {
26    fn generate(
27        &self,
28        groups: &[FixtureGroup],
29        e2e_config: &E2eConfig,
30        _config: &ResolvedCrateConfig,
31    ) -> Result<Vec<GeneratedFile>> {
32        let output_base = PathBuf::from(e2e_config.effective_output()).join(self.language_name());
33        let tests_base = output_base.join("tests");
34
35        let mut files = Vec::new();
36
37        // Resolve call config with overrides — use "node" key (Language::Node).
38        let call = &e2e_config.call;
39        let overrides = call.overrides.get("node");
40        let module_path = overrides
41            .and_then(|o| o.module.as_ref())
42            .cloned()
43            .unwrap_or_else(|| call.module.clone());
44        let function_name = overrides.and_then(|o| o.function.as_ref()).cloned().unwrap_or_else(|| {
45            let default_cc = e2e_config.resolve_call(None);
46            resolve_node_function_name(default_cc)
47        });
48        let client_factory = overrides.and_then(|o| o.client_factory.as_deref());
49
50        // Resolve package config.
51        let node_pkg = e2e_config.resolve_package("node");
52        let pkg_path = node_pkg
53            .as_ref()
54            .and_then(|p| p.path.as_ref())
55            .cloned()
56            .unwrap_or_else(|| "../../packages/typescript".to_string());
57        let pkg_name = node_pkg
58            .as_ref()
59            .and_then(|p| p.name.as_ref())
60            .cloned()
61            .unwrap_or_else(|| module_path.clone());
62        let pkg_version = node_pkg
63            .as_ref()
64            .and_then(|p| p.version.as_ref())
65            .cloned()
66            .unwrap_or_else(|| "0.1.0".to_string());
67
68        let has_http_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| f.is_http_test());
69
70        let has_file_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
71            let cc = e2e_config.resolve_call(f.call.as_deref());
72            cc.args
73                .iter()
74                .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
75        });
76
77        files.push(GeneratedFile {
78            path: output_base.join("package.json"),
79            content: render_package_json(
80                &pkg_name,
81                &pkg_path,
82                &pkg_version,
83                e2e_config.dep_mode,
84                has_http_fixtures,
85            ),
86            generated_header: false,
87        });
88
89        files.push(GeneratedFile {
90            path: output_base.join("tsconfig.json"),
91            content: render_tsconfig(),
92            generated_header: false,
93        });
94
95        let needs_global_setup = client_factory.is_some() || has_http_fixtures;
96
97        files.push(GeneratedFile {
98            path: output_base.join("vitest.config.ts"),
99            content: render_vitest_config(needs_global_setup, has_file_fixtures),
100            generated_header: true,
101        });
102
103        if needs_global_setup {
104            files.push(GeneratedFile {
105                path: output_base.join("globalSetup.ts"),
106                content: render_global_setup(),
107                generated_header: true,
108            });
109        }
110
111        if has_file_fixtures {
112            files.push(GeneratedFile {
113                path: output_base.join("setup.ts"),
114                content: render_file_setup(),
115                generated_header: true,
116            });
117        }
118
119        let options_type = overrides.and_then(|o| o.options_type.clone());
120        let field_resolver = FieldResolver::new(
121            &e2e_config.fields,
122            &e2e_config.fields_optional,
123            &e2e_config.result_fields,
124            &e2e_config.fields_array,
125            &std::collections::HashSet::new(),
126        );
127
128        for group in groups {
129            let active: Vec<_> = group
130                .fixtures
131                .iter()
132                .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip("node")))
133                .collect();
134
135            if active.is_empty() {
136                continue;
137            }
138
139            let filename = format!("{}.test.ts", crate::escape::sanitize_filename(&group.category));
140            let content = render_test_file(
141                "node",
142                &group.category,
143                &active,
144                &module_path,
145                &pkg_name,
146                &function_name,
147                &e2e_config.call.args,
148                options_type.as_deref(),
149                &field_resolver,
150                client_factory,
151                e2e_config,
152            );
153            files.push(GeneratedFile {
154                path: tests_base.join(filename),
155                content,
156                generated_header: true,
157            });
158        }
159
160        Ok(files)
161    }
162
163    fn language_name(&self) -> &'static str {
164        "node"
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn language_name_is_node() {
174        let codegen = TypeScriptCodegen;
175        assert_eq!(codegen.language_name(), "node");
176    }
177
178    #[test]
179    fn generate_empty_groups_produces_config_files_only() {
180        use alef_core::config::NewAlefConfig;
181        let cfg: NewAlefConfig = toml::from_str(
182            r#"
183[workspace]
184languages = ["node"]
185
186[[crates]]
187name = "my-lib"
188sources = ["src/lib.rs"]
189
190[crates.e2e]
191fixtures = "fixtures"
192output = "e2e"
193[crates.e2e.call]
194function = "process"
195module = "my-lib"
196result_var = "result"
197"#,
198        )
199        .unwrap();
200        let e2e = cfg.crates[0].e2e.clone().unwrap();
201        let resolved = cfg.resolve().unwrap().remove(0);
202        let codegen = TypeScriptCodegen;
203        let files = codegen.generate(&[], &e2e, &resolved).unwrap();
204        // package.json, tsconfig.json, vitest.config.ts
205        assert!(files.len() >= 3, "got {} files", files.len());
206    }
207}