use crate::e2e::codegen::assertion_type_skip::{
streaming_assertion_type_skip_line, streaming_assertion_value_skip_line,
};
use crate::e2e::codegen::field_skip::{FieldSkip, nested_wildcard_skip_line};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;
use heck::ToLowerCamelCase;
use std::fmt::Write as FmtWrite;
use super::values::escape_dart;
const BARE_RESULT_TOKEN: &str = "<bare result>";
pub(super) fn dart_length_expr(field_accessor: &str, field: Option<&str>, field_resolver: &FieldResolver) -> String {
let is_optional = field
.map(|f| {
let resolved = field_resolver.resolve(f);
field_resolver.is_optional(f) || field_resolver.is_optional(resolved)
})
.unwrap_or(false);
if is_optional {
format!("{field_accessor}?.length ?? 0")
} else {
format!("{field_accessor}.length")
}
}
pub(super) fn dart_format_value(val: &serde_json::Value) -> String {
match val {
serde_json::Value::String(s) => format!("'{}'", escape_dart(s)),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Null => "null".to_string(),
other => format!("'{}'", escape_dart(&other.to_string())),
}
}
pub(super) fn render_assertion_dart(
out: &mut String,
assertion: &Assertion,
result_var: &str,
result_is_simple: bool,
field_resolver: &FieldResolver,
enum_fields: &std::collections::HashSet<String>,
) {
if !result_is_simple && let Some(f) = assertion.field.as_deref() {
if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
let _ = writeln!(
out,
" // skipped: {}",
FieldSkip::NotAvailableOnDartResultType.message(f)
);
return;
}
}
if let Some(f) = assertion.field.as_deref()
&& !f.is_empty()
&& field_resolver.tagged_union_split(f).is_some()
{
let _ = writeln!(
out,
" // skipped: {}",
FieldSkip::CrossesTaggedUnionBoundaryInDart.message(f)
);
return;
}
if let Some(f) = assertion.field.as_deref()
&& let Some(dot) = f.find("[].")
{
let resolved_full = field_resolver.resolve(f);
let (array_part, elem_part) = match resolved_full.find("[].") {
Some(rdot) => (&resolved_full[..rdot], &resolved_full[rdot + 3..]),
None => (&f[..dot], &f[dot + 3..]),
};
if let Some(line) = nested_wildcard_skip_line(" ", "//", f, elem_part) {
let _ = writeln!(out, "{line}");
return;
}
let array_accessor = if array_part.is_empty() {
result_var.to_string()
} else {
field_resolver.accessor(array_part, "dart", result_var)
};
let elem_accessor = field_to_dart_accessor(elem_part);
match assertion.assertion_type.as_str() {
"contains" => {
if let Some(expected) = &assertion.value {
let dart_val = dart_format_value(expected);
let _ = writeln!(
out,
" expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
);
}
}
"contains_all" => {
if let Some(values) = &assertion.values {
for val in values {
let dart_val = dart_format_value(val);
let _ = writeln!(
out,
" expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
);
}
}
}
"not_contains" => {
if let Some(expected) = &assertion.value {
let dart_val = dart_format_value(expected);
let _ = writeln!(
out,
" expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
);
} else if let Some(values) = &assertion.values {
for val in values {
let dart_val = dart_format_value(val);
let _ = writeln!(
out,
" expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
);
}
}
}
"not_empty" => {
let _ = writeln!(
out,
" expect({array_accessor}.any((e) => e.{elem_accessor}.toString().isNotEmpty), isTrue);"
);
}
other => {
let _ = writeln!(
out,
" // skipped: unsupported traversal assertion '{other}' on '{f}'"
);
}
}
return;
}
let field_accessor = if result_is_simple {
result_var.to_string()
} else {
match assertion.field.as_deref() {
Some(f) if !f.is_empty() => field_resolver.accessor(f, "dart", result_var),
_ => result_var.to_string(),
}
};
let format_value = |val: &serde_json::Value| -> String { dart_format_value(val) };
match assertion.assertion_type.as_str() {
"equals" | "field_equals" => {
if let Some(expected) = &assertion.value {
let dart_val = format_value(expected);
let is_enum_field = assertion
.field
.as_deref()
.map(|f| {
let resolved = field_resolver.resolve(f);
enum_fields.contains(f) || enum_fields.contains(resolved)
})
.unwrap_or(false);
let is_display_as_text = assertion
.field
.as_deref()
.map(|f| {
let resolved = field_resolver.resolve(f);
field_resolver.is_display_as_text(f) || field_resolver.is_display_as_text(resolved)
})
.unwrap_or(false);
if expected.is_string() {
if is_enum_field {
let _ = writeln!(
out,
" expect(_alefE2eText({field_accessor}), equals({dart_val}.toString()));"
);
} else if is_display_as_text {
let is_optional = assertion
.field
.as_deref()
.map(|f| {
let resolved = field_resolver.resolve(f);
field_resolver.is_optional(f) || field_resolver.is_optional(resolved)
})
.unwrap_or(false);
if is_optional {
let _ = writeln!(
out,
" expect(({field_accessor}?.text() ?? ''), equals({dart_val}.toString()));"
);
} else {
let _ = writeln!(
out,
" expect({field_accessor}.text(), equals({dart_val}.toString()));"
);
}
} else {
let safe_accessor = if result_is_simple && assertion.field.is_none() {
format!("({field_accessor} ?? '').toString()")
} else {
format!("{field_accessor}.toString()")
};
let _ = writeln!(out, " expect({safe_accessor}, equals({dart_val}.toString()));");
}
} else {
let _ = writeln!(out, " expect({field_accessor}, equals({dart_val}));");
}
} else {
let _ = writeln!(
out,
" // skipped: '{}' assertion missing value",
assertion.assertion_type
);
}
}
"not_equals" => {
if let Some(expected) = &assertion.value {
let dart_val = format_value(expected);
let is_enum_field = assertion
.field
.as_deref()
.map(|f| {
let resolved = field_resolver.resolve(f);
enum_fields.contains(f) || enum_fields.contains(resolved)
})
.unwrap_or(false);
let is_display_as_text = assertion
.field
.as_deref()
.map(|f| {
let resolved = field_resolver.resolve(f);
field_resolver.is_display_as_text(f) || field_resolver.is_display_as_text(resolved)
})
.unwrap_or(false);
if expected.is_string() {
if is_enum_field {
let _ = writeln!(
out,
" expect(_alefE2eText({field_accessor}).trim(), isNot(equals({dart_val}.toString().trim())));"
);
} else if is_display_as_text {
let is_optional = assertion
.field
.as_deref()
.map(|f| {
let resolved = field_resolver.resolve(f);
field_resolver.is_optional(f) || field_resolver.is_optional(resolved)
})
.unwrap_or(false);
if is_optional {
let _ = writeln!(
out,
" expect(({field_accessor}?.text() ?? '').trim(), isNot(equals({dart_val}.toString().trim())));"
);
} else {
let _ = writeln!(
out,
" expect({field_accessor}.text().trim(), isNot(equals({dart_val}.toString().trim())));"
);
}
} else {
let safe_accessor = if result_is_simple && assertion.field.is_none() {
format!("({field_accessor} ?? '').toString().trim()")
} else {
format!("{field_accessor}.toString().trim()")
};
let _ = writeln!(
out,
" expect({safe_accessor}, isNot(equals({dart_val}.toString().trim())));"
);
}
} else {
let _ = writeln!(out, " expect({field_accessor}, isNot(equals({dart_val})));");
}
}
}
"contains" => {
if let Some(expected) = &assertion.value {
let dart_val = format_value(expected);
let aggregator = dart_stringy_aggregator_contains_assert(
assertion.field.as_deref(),
result_var,
field_resolver,
&dart_val,
);
if let Some(line) = aggregator {
let _ = writeln!(out, "{line}");
} else {
let _ = writeln!(out, " expect({field_accessor}, contains({dart_val}));");
}
} else {
let _ = writeln!(out, " // skipped: 'contains' assertion missing value");
}
}
"contains_all" => {
if let Some(values) = &assertion.values {
for val in values {
let dart_val = format_value(val);
let _ = writeln!(out, " expect({field_accessor}, contains({dart_val}));");
}
}
}
"contains_any" => {
if let Some(values) = &assertion.values {
let checks: Vec<String> = values
.iter()
.map(|v| {
let dart_val = format_value(v);
format!("{field_accessor}.contains({dart_val})")
})
.collect();
let joined = checks.join(" || ");
let _ = writeln!(out, " expect({joined}, isTrue);");
}
}
"not_contains" => {
if let Some(expected) = &assertion.value {
let dart_val = format_value(expected);
let _ = writeln!(out, " expect({field_accessor}, isNot(contains({dart_val})));");
} else if let Some(values) = &assertion.values {
for val in values {
let dart_val = format_value(val);
let _ = writeln!(out, " expect({field_accessor}, isNot(contains({dart_val})));");
}
}
}
"not_empty" => {
let is_collection = assertion.field.as_deref().is_some_and(|f| {
let resolved = field_resolver.resolve(f);
field_resolver.is_array(f) || field_resolver.is_array(resolved)
});
if is_collection {
let _ = writeln!(out, " expect({field_accessor}, isNotEmpty);");
} else {
let _ = writeln!(out, " expect({field_accessor}?.toString(), isNotEmpty);");
}
}
"is_empty" => {
let _ = writeln!(out, " expect({field_accessor}, anyOf(isNull, isEmpty));");
}
"starts_with" => {
if let Some(expected) = &assertion.value {
let dart_val = format_value(expected);
let _ = writeln!(out, " expect({field_accessor}, startsWith({dart_val}));");
}
}
"ends_with" => {
if let Some(expected) = &assertion.value {
let dart_val = format_value(expected);
let _ = writeln!(out, " expect({field_accessor}, endsWith({dart_val}));");
}
}
"min_length" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
let _ = writeln!(out, " expect({length_expr}, greaterThanOrEqualTo({n}));");
}
}
"max_length" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
let _ = writeln!(out, " expect({length_expr}, lessThanOrEqualTo({n}));");
}
}
"count_equals" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
let _ = writeln!(out, " expect({length_expr}, equals({n}));");
}
}
"count_min" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
let _ = writeln!(out, " expect({length_expr}, greaterThanOrEqualTo({n}));");
}
}
"matches_regex" => {
if let Some(expected) = &assertion.value {
let dart_val = format_value(expected);
let _ = writeln!(out, " expect({field_accessor}, matches(RegExp({dart_val})));");
}
}
"is_true" => {
let is_optional = assertion.field.as_ref().is_some_and(|f| {
let resolved = field_resolver.resolve(f);
field_resolver.is_optional(f) || field_resolver.is_optional(resolved)
});
if is_optional {
let _ = writeln!(out, " expect({field_accessor}, isNotNull);");
} else {
let _ = writeln!(out, " expect({field_accessor}, isTrue);");
}
}
"is_false" => {
let is_optional = assertion.field.as_ref().is_some_and(|f| {
let resolved = field_resolver.resolve(f);
field_resolver.is_optional(f) || field_resolver.is_optional(resolved)
});
if is_optional {
let _ = writeln!(out, " expect({field_accessor}, isNull);");
} else {
let _ = writeln!(out, " expect({field_accessor}, isFalse);");
}
}
"greater_than" => {
if let Some(val) = &assertion.value {
let dart_val = format_value(val);
let _ = writeln!(out, " expect({field_accessor}, greaterThan({dart_val}));");
}
}
"less_than" => {
if let Some(val) = &assertion.value {
let dart_val = format_value(val);
let _ = writeln!(out, " expect({field_accessor}, lessThan({dart_val}));");
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let dart_val = format_value(val);
let _ = writeln!(out, " expect({field_accessor}, greaterThanOrEqualTo({dart_val}));");
}
}
"less_than_or_equal" => {
if let Some(val) = &assertion.value {
let dart_val = format_value(val);
let _ = writeln!(out, " expect({field_accessor}, lessThanOrEqualTo({dart_val}));");
}
}
"not_null" => {
let _ = writeln!(out, " expect({field_accessor}, isNotNull);");
}
"not_error" => {
}
"error" => {
}
"method_result" => {
if let Some(method) = &assertion.method {
let dart_method = method.to_lower_camel_case();
let check = assertion.check.as_deref().unwrap_or("not_null");
let method_call = format!("{field_accessor}.{dart_method}()");
match check {
"equals" => {
if let Some(expected) = &assertion.value {
let dart_val = format_value(expected);
let _ = writeln!(out, " expect({method_call}, equals({dart_val}));");
}
}
"is_true" => {
let _ = writeln!(out, " expect({method_call}, isTrue);");
}
"is_false" => {
let _ = writeln!(out, " expect({method_call}, isFalse);");
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let dart_val = format_value(val);
let _ = writeln!(out, " expect({method_call}, greaterThanOrEqualTo({dart_val}));");
}
}
"count_min" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(out, " expect({method_call}.length, greaterThanOrEqualTo({n}));");
}
}
_ => {
let _ = writeln!(out, " expect({method_call}, isNotNull);");
}
}
}
}
other => {
let _ = writeln!(out, " // skipped: unknown assertion type '{other}'");
}
}
}
pub(super) fn render_streaming_assertion_dart(out: &mut String, assertion: &Assertion, result_var: &str) {
match assertion.assertion_type.as_str() {
"not_error" => {
let _ = writeln!(out, " expect({result_var}, isNotNull);");
}
"count_min" if assertion.field.as_deref() == Some("chunks") => {
if let Some(serde_json::Value::Number(n)) = &assertion.value {
let _ = writeln!(out, " expect({result_var}.length, greaterThanOrEqualTo({n}));");
} else {
let _ = writeln!(
out,
"{}",
streaming_assertion_value_skip_line(" ", "//", "chunks", &assertion.assertion_type)
);
}
}
"equals" if assertion.field.as_deref() == Some("stream_content") => {
if let Some(serde_json::Value::String(expected)) = &assertion.value {
let escaped = escape_dart(expected);
let _ = writeln!(
out,
" final _content = {result_var}.map((c) => c.choices.firstOrNull?.delta.content ?? '').join();"
);
let _ = writeln!(out, " expect(_content, equals('{escaped}'));");
} else {
let _ = writeln!(
out,
"{}",
streaming_assertion_value_skip_line(" ", "//", "stream_content", &assertion.assertion_type)
);
}
}
_ => {
let _ = writeln!(
out,
"{}",
streaming_assertion_type_skip_line(
" ",
"//",
assertion.field.as_deref().unwrap_or(BARE_RESULT_TOKEN),
&assertion.assertion_type
)
);
}
}
}
pub(super) fn snake_to_camel(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut next_upper = false;
for ch in s.chars() {
if ch == '_' {
next_upper = true;
} else if next_upper {
result.extend(ch.to_uppercase());
next_upper = false;
} else {
result.push(ch);
}
}
result
}
pub(super) fn field_to_dart_accessor(path: &str) -> String {
let mut result = String::with_capacity(path.len());
for (i, segment) in path.split('.').enumerate() {
if i > 0 {
result.push('.');
}
if let Some(bracket_pos) = segment.find('[') {
let name = &segment[..bracket_pos];
let bracket = &segment[bracket_pos..];
result.push_str(&name.to_lower_camel_case());
result.push('!');
result.push_str(bracket);
} else {
result.push_str(&segment.to_lower_camel_case());
}
}
result
}
pub(super) fn dart_stringy_aggregator_contains_assert(
field: Option<&str>,
result_var: &str,
field_resolver: &crate::e2e::field_access::FieldResolver,
dart_val: &str,
) -> Option<String> {
use crate::e2e::field_access::StringyFieldKind;
let field = field?;
let resolved = field_resolver.resolve(field);
if resolved.contains('.') || resolved.contains('[') {
return None;
}
if !field_resolver.is_array(field) && !field_resolver.is_array(resolved) {
return None;
}
let array_accessor = field_resolver.accessor(field, "dart", result_var);
let root_type = field_resolver.dart_root_type().cloned();
if let Some(elem_type) = field_resolver.dart_advance(root_type.as_deref(), resolved)
&& let Some(stringy) = field_resolver.dart_stringy_fields(&elem_type)
{
if stringy.len() >= 2 {
let mut texts_lines: Vec<String> = Vec::new();
for sf in stringy {
let call = sf.name.to_lower_camel_case();
match sf.kind {
StringyFieldKind::Plain => {
texts_lines.push(format!(" texts.add(item.{call}.toString());"));
}
StringyFieldKind::Optional => {
texts_lines.push(format!(
" final v_{call} = item.{call};\n if (v_{call} != null) texts.add(v_{call}.toString());"
));
}
StringyFieldKind::Vec => {
texts_lines.push(format!(
" texts.addAll(item.{call}.map((e) => e.toString()));"
));
}
}
}
let texts_block = texts_lines.join("\n");
return Some(format!(
" expect({array_accessor}.where((item) {{\n final texts = <String>[];\n{texts_block}\n return texts.any((t) => t.toLowerCase().contains(({dart_val}).toString().toLowerCase()));\n }}).isEmpty, isFalse);"
));
}
}
Some(format!(
" expect({array_accessor}.where((item) => item.toString().toLowerCase().contains(({dart_val}).toString().toLowerCase())).isEmpty, isFalse);"
))
}
#[cfg(test)]
mod wildcard_tests {
use super::{field_to_dart_accessor, render_assertion_dart};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;
use std::collections::{HashMap, HashSet};
fn array_resolver(field: &str) -> FieldResolver {
let names: HashSet<String> = [field.to_string()].into_iter().collect();
FieldResolver::new(&HashMap::new(), &HashSet::new(), &names, &names, &HashSet::new())
}
fn render_contains(resolver: &FieldResolver, field: &str, value: &str) -> String {
let assertion = Assertion {
assertion_type: "contains".to_string(),
field: Some(field.to_string()),
value: Some(serde_json::Value::String(value.to_string())),
..Assertion::default()
};
let mut out = String::new();
render_assertion_dart(&mut out, &assertion, "result", false, resolver, &HashSet::new());
out
}
#[test]
fn single_wildcard_still_quantifies_over_every_element() {
let out = render_contains(&array_resolver("links"), "links[].url", "example.test");
assert!(out.contains("result.links.any((e) => e.url"), "got: {out}");
}
#[test]
fn the_element_accessor_renders_a_surviving_wildcard_as_invalid_dart() {
assert_eq!(field_to_dart_accessor("links[].url"), "links![].url");
}
#[test]
fn nested_wildcard_should_emit_a_visible_skip_rather_than_an_index_zero_check() {
let out = render_contains(&array_resolver("pages"), "pages[].links[].url", "example.test");
assert_eq!(
out, " // skipped: nested array-wildcard field 'pages[].links[].url' not supported\n",
"got: {out}"
);
}
fn resolver_with_alias(alias_from: &str, alias_to: &str, result_field: &str) -> FieldResolver {
let aliases: HashMap<String, String> = [(alias_from.to_string(), alias_to.to_string())].into_iter().collect();
let result_fields: HashSet<String> = [result_field.to_string()].into_iter().collect();
FieldResolver::new(
&aliases,
&HashSet::new(),
&result_fields,
&HashSet::new(),
&HashSet::new(),
)
}
#[test]
fn alias_renaming_the_array_head_segment_still_resolves() {
let out = render_contains(
&resolver_with_alias("hreflang[].lang", "metadata.hreflangs[].lang", "metadata"),
"hreflang[].lang",
"en",
);
assert!(!out.contains("skipped"), "got: {out}");
assert!(
out.contains("result.metadata.hreflangs.any((e) => e.lang"),
"got: {out}"
);
}
#[test]
fn alias_renaming_only_the_sub_field_still_resolves() {
let out = render_contains(
&resolver_with_alias("assets[].category", "assets[].asset_category", "assets"),
"assets[].category",
"books",
);
assert!(!out.contains("skipped"), "got: {out}");
assert!(out.contains("result.assets.any((e) => e.assetCategory"), "got: {out}");
}
}
#[cfg(test)]
mod is_true_optional_field_tests {
use super::render_assertion_dart;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;
use std::collections::{HashMap, HashSet};
fn optional_resolver(field: &str) -> FieldResolver {
let optional: HashSet<String> = [field.to_string()].into_iter().collect();
FieldResolver::new(
&HashMap::new(),
&optional,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
)
}
fn render(resolver: &FieldResolver, assertion: &Assertion) -> String {
let mut out = String::new();
render_assertion_dart(&mut out, assertion, "result", false, resolver, &HashSet::new());
out
}
fn is_true_assertion(field: &str) -> Assertion {
Assertion {
assertion_type: "is_true".to_string(),
field: Some(field.to_string()),
..Assertion::default()
}
}
#[test]
fn is_true_on_optional_struct_field_checks_presence() {
let out = render(&optional_resolver("data"), &is_true_assertion("data"));
assert_eq!(out, " expect(result.data, isNotNull);\n");
}
#[test]
fn is_false_on_optional_struct_field_checks_absence() {
let out = render(
&optional_resolver("data"),
&Assertion {
assertion_type: "is_false".to_string(),
field: Some("data".to_string()),
..Assertion::default()
},
);
assert_eq!(out, " expect(result.data, isNull);\n");
}
#[test]
fn is_true_on_non_optional_field_is_unchanged() {
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
let out = render(&resolver, &is_true_assertion("active"));
assert_eq!(out, " expect(result.active, isTrue);\n");
}
}