use crate::core::config::ResolvedCrateConfig;
use crate::e2e::codegen::resolve_field;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;
use std::fmt::Write as FmtWrite;
use super::assertions::{render_assertion_dart, render_streaming_assertion_dart};
use super::values::{escape_dart, mime_from_extension, type_name_to_create_from_json_dart};
const COMPATIBLE_OPTIONS_TYPE_LANGS: &[&str] = &["csharp", "c", "go", "java", "php", "python", "r"];
mod args;
fn dart_error_matcher(
out: &mut String,
indent: &str,
fixture: &Fixture,
errors: &[crate::core::ir::ErrorDef],
) -> String {
use crate::e2e::codegen::declared_error_variant::{DeclaredErrorAssertion, classify, skip_line};
match classify("dart", fixture, errors) {
DeclaredErrorAssertion::Undeclared => "throwsA(anything)".to_string(),
DeclaredErrorAssertion::Assert(declared) => {
let escaped = escape_dart(declared);
format!(
"throwsA(predicate((e) => e.toString().contains('{escaped}') || e.runtimeType.toString().contains('{escaped}')))"
)
}
DeclaredErrorAssertion::Unsubstantiable(variant) => {
let _ = writeln!(out, "{}", skip_line(indent, "//", variant, &fixture.id, "dart"));
"throwsA(anything)".to_string()
}
}
}
fn has_real_dart_assertion(body: &str) -> bool {
body.lines().any(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with("//")
})
}
fn snake_to_lower_camel_case(name: &str) -> String {
name.split('_')
.enumerate()
.map(|(i, part)| {
if i == 0 {
part.to_string()
} else {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
})
.collect::<Vec<_>>()
.join("")
}
struct VisitorArgContext<'a> {
config: &'a ResolvedCrateConfig,
call_overrides: Option<&'a crate::e2e::config::CallOverride>,
type_defs: &'a [crate::core::ir::TypeDef],
options_type: Option<&'a str>,
}
fn apply_fixture_visitor(
setup_lines: &mut Vec<String>,
args: &mut Vec<String>,
fixture: &Fixture,
ctx: &VisitorArgContext<'_>,
) {
let options_type = ctx.options_type;
if let Some(visitor_spec) = &fixture.visitor {
let mut visitor_setup: Vec<String> = Vec::new();
let visitor_config = crate::e2e::codegen::dart_visitors::resolve_dart_visitor_config(
ctx.config,
ctx.call_overrides,
ctx.type_defs,
visitor_spec,
);
let _ =
crate::e2e::codegen::dart_visitors::build_dart_visitor(&mut visitor_setup, visitor_spec, &visitor_config);
for line in visitor_setup.into_iter().rev() {
setup_lines.insert(0, line);
}
let already_has_options = args.iter().any(|a| a.starts_with("options:") || a == "options");
if !already_has_options {
if let Some(opts_type) = options_type {
let dart_fn = type_name_to_create_from_json_dart(opts_type);
setup_lines.push(format!(
"final options = await {dart_fn}WithVisitor(json: '{{}}', visitor: visitor);"
));
args.push("options: options".to_string());
}
} else if let Some(opts_type) = options_type {
let dart_fn = type_name_to_create_from_json_dart(opts_type);
let needle = format!("await {dart_fn}(json:");
let replacement = format!("await {dart_fn}WithVisitor(visitor: visitor, json:");
for line in setup_lines.iter_mut() {
if line.contains(&needle) {
*line = line.replace(&needle, &replacement);
}
}
}
}
}
fn resolve_dart_receiver(
client_factory_camel: Option<&str>,
is_snippet: bool,
fixture: &Fixture,
receiver_class: &str,
call_config: &crate::e2e::config::CallConfig,
fixture_id: &str,
) -> (String, Option<String>) {
if let Some(factory) = client_factory_camel {
if is_snippet {
let api_key_var = crate::e2e::fixture::FixtureEnv::api_key_var_or_default(fixture.env.as_ref());
let base_url_arg = crate::e2e::codegen::client_factory::docs_base_url(fixture.docs_client())
.map(|base_url| format!(", baseUrl: '{}'", escape_dart(base_url)))
.unwrap_or_default();
let create_line = format!(
"final apiKey = Platform.environment['{api_key_var}'];\n if (apiKey == null || apiKey.isEmpty) {{ throw StateError('{api_key_var} must be set'); }}\n final client = await {receiver_class}.{factory}(apiKey{base_url_arg});"
);
("client".to_string(), Some(create_line))
} else {
let has_mock_url = fixture
.resolved_args(call_config)
.iter()
.any(|a| a.arg_type == "mock_url");
let mock_url_setup = if !has_mock_url {
Some(format!(r#"final mockUrl = _fixtureUrl("{fixture_id}");"#))
} else {
None
};
let url_expr = if has_mock_url {
call_config
.args
.iter()
.find(|a| a.arg_type == "mock_url")
.map(|a| a.name.clone())
.unwrap_or_else(|| "mockUrl".to_string())
} else {
"mockUrl".to_string()
};
let create_line =
format!("final client = await {receiver_class}.{factory}('test-key', baseUrl: {url_expr});");
let full_setup = if let Some(url_line) = mock_url_setup {
Some(format!("{url_line}\n {create_line}"))
} else {
Some(create_line)
};
("client".to_string(), full_setup)
}
} else {
(receiver_class.to_string(), None)
}
}
pub(super) struct DartTestCaseContext<'a> {
pub(super) e2e_config: &'a E2eConfig,
pub(super) lang: &'a str,
pub(super) bridge_class: &'a str,
pub(super) dart_first_class_map: &'a crate::e2e::field_access::DartFirstClassMap,
pub(super) adapters: &'a [crate::core::config::extras::AdapterConfig],
pub(super) config: &'a ResolvedCrateConfig,
pub(super) type_defs: &'a [crate::core::ir::TypeDef],
pub(super) enums: &'a [crate::core::ir::EnumDef],
pub(super) functions: &'a [crate::core::ir::FunctionDef],
pub(super) errors: &'a [crate::core::ir::ErrorDef],
pub(super) native_typed_dtos: bool,
pub(super) is_snippet: bool,
}
pub(super) fn render_test_case(out: &mut String, fixture: &Fixture, context: DartTestCaseContext<'_>) {
let DartTestCaseContext {
e2e_config,
lang,
bridge_class,
dart_first_class_map,
adapters,
config,
type_defs,
enums,
functions,
errors,
native_typed_dtos,
is_snippet,
} = context;
if let Some(http) = &fixture.http {
super::http::render_http_test_case(out, fixture, http);
return;
}
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let call_recipe =
crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve(lang, fixture, call_config, type_defs)
.with_functions(functions);
let target_params = call_recipe.target_params(lang);
let call_overrides = call_config.overrides.get(lang);
let call_field_resolver = super::call_field_resolver::build_call_field_resolver(
e2e_config,
call_config,
fixture,
lang,
dart_first_class_map,
type_defs,
enums,
functions,
);
let field_resolver = &call_field_resolver;
let mut function_name = call_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call_config.function.clone());
function_name = snake_to_lower_camel_case(&function_name);
let result_var = call_config.effective_result_var();
let description = escape_dart(&fixture.description);
let fixture_id = &fixture.id;
let _is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let is_streaming =
crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming_enabled());
let result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || call_config.result_is_simple;
let options_type: Option<&str> = call_recipe.compatible_options_type(COMPATIBLE_OPTIONS_TYPE_LANGS);
let options_via: &str = call_recipe.options_via;
let file_path_for_mime: Option<&str> = fixture
.resolved_args(call_config)
.iter()
.find(|a| a.arg_type == "file_path")
.and_then(|a| resolve_field(&fixture.input, &a.field).as_str());
let has_file_path_arg = fixture
.resolved_args(call_config)
.iter()
.any(|a| a.arg_type == "file_path");
let routes_to_source_code = file_path_for_mime
.and_then(mime_from_extension)
.map(|m| m == "text/x-source-code")
.unwrap_or(false);
let caller_supplied_override = call_overrides.and_then(|o| o.function.as_ref()).is_some();
if has_file_path_arg && !caller_supplied_override && !routes_to_source_code {
function_name = match function_name.as_str() {
"extractFile" => "extractBytes".to_string(),
"extractFileSync" => "extractBytesSync".to_string(),
other => other.to_string(),
};
}
let client_factory_for_args: Option<&str> =
call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
e2e_config
.call
.overrides
.get(lang)
.and_then(|o| o.client_factory.as_deref())
});
let is_frb_bridge_call = false;
let adapter_lookup_name = call_config.core_lookup_name(lang);
let adapter = adapter_lookup_name
.as_deref()
.and_then(|name| adapters.iter().find(|a| a.name == name));
let adapter_request_type: Option<String> = adapter
.and_then(|a| a.request_type.as_deref())
.map(|rt| rt.rsplit("::").next().unwrap_or(rt).to_string());
let mut setup_lines: Vec<String> = Vec::new();
let mut args = Vec::new();
let arg_ctx = args::DartArgContext {
fixture,
is_snippet,
bridge_class,
config,
type_defs,
enums,
call_recipe: &call_recipe,
target_params,
native_typed_dtos,
options_type,
options_via,
is_frb_bridge_call,
adapter_request_type: adapter_request_type.as_deref(),
file_path_for_mime,
routes_to_source_code,
client_factory_for_args,
};
args::build_args_and_setup(&mut setup_lines, &mut args, &arg_ctx);
let visitor_ctx = VisitorArgContext {
config,
call_overrides,
type_defs,
options_type,
};
apply_fixture_visitor(&mut setup_lines, &mut args, fixture, &visitor_ctx);
let client_factory: Option<&str> = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
e2e_config
.call
.overrides
.get(lang)
.and_then(|o| o.client_factory.as_deref())
});
let client_factory_camel: Option<String> = client_factory.map(snake_to_lower_camel_case);
let _ = writeln!(out, " test('{description}', () async {{");
let args_str = args.join(", ");
let receiver_class = call_overrides
.and_then(|o| o.class.as_ref())
.cloned()
.unwrap_or_else(|| bridge_class.to_string());
let (receiver, extra_setup): (String, Option<String>) = resolve_dart_receiver(
client_factory_camel.as_deref(),
is_snippet,
fixture,
&receiver_class,
call_config,
fixture_id,
);
let emission_ctx = CallEmissionContext {
setup_lines: &setup_lines,
extra_setup: &extra_setup,
is_streaming,
receiver: &receiver,
function_name: &function_name,
args_str: &args_str,
fixture,
errors,
call_config,
result_binding: result_var,
field_resolver,
result_is_simple,
expects_error,
};
emit_call_and_assertions(out, &emission_ctx);
let _ = writeln!(out, " }});");
let _ = writeln!(out);
}
struct CallEmissionContext<'a> {
setup_lines: &'a [String],
extra_setup: &'a Option<String>,
is_streaming: bool,
receiver: &'a str,
function_name: &'a str,
args_str: &'a str,
fixture: &'a Fixture,
errors: &'a [crate::core::ir::ErrorDef],
call_config: &'a crate::e2e::config::CallConfig,
result_binding: &'a str,
field_resolver: &'a crate::e2e::field_access::FieldResolver,
result_is_simple: bool,
expects_error: bool,
}
fn emit_call_and_assertions(out: &mut String, ctx: &CallEmissionContext<'_>) {
let setup_lines = ctx.setup_lines;
let extra_setup = ctx.extra_setup;
let is_streaming = ctx.is_streaming;
let receiver = ctx.receiver;
let function_name = ctx.function_name;
let args_str = ctx.args_str;
let fixture = ctx.fixture;
let errors = ctx.errors;
let call_config = ctx.call_config;
let result_var = ctx.result_binding;
let field_resolver = ctx.field_resolver;
let result_is_simple = ctx.result_is_simple;
if ctx.expects_error && (!setup_lines.is_empty() || extra_setup.is_some()) {
let _ = writeln!(out, " await expectLater(() async {{");
for line in setup_lines {
for inner_line in line.lines() {
let _ = writeln!(out, " {inner_line}");
}
}
if let Some(extra) = extra_setup {
for line in extra.lines() {
let _ = writeln!(out, " {line}");
}
}
if is_streaming {
let _ = writeln!(out, " return {receiver}.{function_name}({args_str}).toList();");
} else {
let _ = writeln!(out, " return {receiver}.{function_name}({args_str});");
}
let matcher = dart_error_matcher(out, " ", fixture, errors);
let _ = writeln!(out, " }}(), {matcher});");
} else if ctx.expects_error {
if let Some(extra) = extra_setup {
for line in extra.lines() {
let _ = writeln!(out, " {line}");
}
}
let matcher = dart_error_matcher(out, " ", fixture, errors);
if is_streaming {
let _ = writeln!(
out,
" await expectLater({receiver}.{function_name}({args_str}).toList(), {matcher});"
);
} else {
let _ = writeln!(
out,
" await expectLater({receiver}.{function_name}({args_str}), {matcher});"
);
}
} else {
for line in setup_lines {
for inner_line in line.lines() {
let _ = writeln!(out, " {inner_line}");
}
}
if let Some(extra) = extra_setup {
for line in extra.lines() {
let _ = writeln!(out, " {line}");
}
}
let void_not_error = fixture
.assertions
.iter()
.any(|assertion| assertion.assertion_type == "not_error");
if call_config.returns_void && void_not_error {
out.push_str(&crate::e2e::template_env::render(
"dart/void_not_error_call.jinja",
minijinja::context! { receiver => receiver, function_name => function_name, args => args_str },
));
} else if call_config.returns_void {
let _ = writeln!(out, " await {receiver}.{function_name}({args_str});");
} else if is_streaming {
let _ = writeln!(
out,
" final {result_var} = await {receiver}.{function_name}({args_str}).toList();"
);
} else {
let _ = writeln!(
out,
" final {result_var} = await {receiver}.{function_name}({args_str});"
);
}
let assertions_start = out.len();
for assertion in &fixture.assertions {
if is_streaming {
render_streaming_assertion_dart(out, assertion, result_var);
} else {
render_assertion_dart(out, assertion, result_var, result_is_simple, field_resolver);
}
}
if !is_streaming
&& !call_config.returns_void
&& !fixture.assertions.is_empty()
&& !has_real_dart_assertion(&out[assertions_start..])
{
let _ = writeln!(out, " expect({result_var}, isNotNull);");
}
crate::e2e::codegen::fail_on_unavailable_field_markers(
&out[assertions_start..],
"dart",
&fixture.id,
&fixture.assertions,
);
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&out[assertions_start..], "dart", &fixture.id);
}
}
#[cfg(test)]
mod tests;