diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/track_fn_optimization.rs b/crates/oxc_angular_compiler/src/pipeline/phases/track_fn_optimization.rs
index cb9f0f178..4e0aacd5f 100644
--- a/crates/oxc_angular_compiler/src/pipeline/phases/track_fn_optimization.rs
+++ b/crates/oxc_angular_compiler/src/pipeline/phases/track_fn_optimization.rs
@@ -94,19 +94,24 @@ fn optimize_track_expression<'a>(
// to the non-optimizable case below, which creates a wrapper function like:
// `function _forTrack($index,$item) { return this.trackByFn; }`
- // First check if the expression contains any ContextExpr or AST expressions
- // that reference the component instance (implicit receiver)
- let has_context = expression_contains_context(&rep.track, expressions);
- if has_context {
- rep.uses_component_instance = true;
- }
-
- // For non-optimizable tracks, replace ContextExpr with TrackContextExpr
- // This signals that context reads in track expressions need special handling
+ // The track function could not be optimized.
+ // Replace context reads with TrackContextExpr, since context reads in a track
+ // function are emitted specially (as `this` instead of `ctx`).
+ //
+ // Following Angular's implementation (track_fn_optimization.ts:54-70), we set
+ // usesComponentInstance inside the transform callback when a ContextExpr is found.
+ // This is the authoritative detection — transformExpressionsInExpression traverses
+ // all expression variants, so no ContextExpr can be missed regardless of nesting.
+ //
+ // By phase 34 (this phase), resolve_names (phase 31) has already converted all
+ // ImplicitReceiver AST nodes into Context IR expressions, so checking for Context
+ // during the transform is sufficient.
+ let found_context = std::cell::Cell::new(false);
transform_expressions_in_expression(
&mut rep.track,
&|expr, _flags| {
if let IrExpression::Context(ctx) = expr {
+ found_context.set(true);
*expr = IrExpression::TrackContext(oxc_allocator::Box::new_in(
TrackContextExpr { view: ctx.view, source_span: None },
allocator,
@@ -115,6 +120,9 @@ fn optimize_track_expression<'a>(
},
VisitorContextFlag::NONE,
);
+ if found_context.get() {
+ rep.uses_component_instance = true;
+ }
// Also create an op list for the tracking expression since it may need
// additional ops when generating the final code (e.g. temporary variables).
@@ -146,179 +154,6 @@ fn optimize_track_expression<'a>(
rep.track_by_ops = Some(track_by_ops);
}
-/// Check if an expression contains any Context expressions or AST expressions that
-/// reference the component instance (implicit receiver).
-fn expression_contains_context(
- expr: &IrExpression<'_>,
- expressions: &crate::pipeline::expression_store::ExpressionStore<'_>,
-) -> bool {
- match expr {
- IrExpression::Context(_) => true,
- // Check AST expressions for implicit receiver usage (this.property, this.method())
- IrExpression::Ast(ast) => ast_contains_implicit_receiver(ast),
- // Check ExpressionRef by looking up the stored expression
- IrExpression::ExpressionRef(id) => {
- let stored_expr = expressions.get(*id);
- ast_contains_implicit_receiver(stored_expr)
- }
- // Resolved expressions (created by resolveNames phase)
- IrExpression::ResolvedCall(rc) => {
- expression_contains_context(&rc.receiver, expressions)
- || rc.args.iter().any(|e| expression_contains_context(e, expressions))
- }
- IrExpression::ResolvedPropertyRead(rp) => {
- expression_contains_context(&rp.receiver, expressions)
- }
- IrExpression::ResolvedBinary(rb) => {
- expression_contains_context(&rb.left, expressions)
- || expression_contains_context(&rb.right, expressions)
- }
- IrExpression::ResolvedKeyedRead(rk) => {
- expression_contains_context(&rk.receiver, expressions)
- || expression_contains_context(&rk.key, expressions)
- }
- IrExpression::ResolvedSafePropertyRead(rsp) => {
- expression_contains_context(&rsp.receiver, expressions)
- }
- IrExpression::SafeTernary(st) => {
- expression_contains_context(&st.guard, expressions)
- || expression_contains_context(&st.expr, expressions)
- }
- IrExpression::SafePropertyRead(sp) => {
- expression_contains_context(&sp.receiver, expressions)
- }
- IrExpression::SafeKeyedRead(sk) => {
- expression_contains_context(&sk.receiver, expressions)
- || expression_contains_context(&sk.index, expressions)
- }
- IrExpression::SafeInvokeFunction(sf) => {
- expression_contains_context(&sf.receiver, expressions)
- || sf.args.iter().any(|e| expression_contains_context(e, expressions))
- }
- IrExpression::PipeBinding(pb) => {
- pb.args.iter().any(|e| expression_contains_context(e, expressions))
- }
- IrExpression::PipeBindingVariadic(pbv) => {
- expression_contains_context(&pbv.args, expressions)
- }
- IrExpression::PureFunction(pf) => {
- pf.args.iter().any(|e| expression_contains_context(e, expressions))
- }
- IrExpression::Interpolation(i) => {
- i.expressions.iter().any(|e| expression_contains_context(e, expressions))
- }
- IrExpression::ResetView(rv) => expression_contains_context(&rv.expr, expressions),
- IrExpression::ConditionalCase(cc) => {
- cc.expr.as_ref().is_some_and(|e| expression_contains_context(e, expressions))
- }
- IrExpression::TwoWayBindingSet(tbs) => {
- expression_contains_context(&tbs.target, expressions)
- || expression_contains_context(&tbs.value, expressions)
- }
- IrExpression::StoreLet(sl) => expression_contains_context(&sl.value, expressions),
- IrExpression::ConstCollected(cc) => expression_contains_context(&cc.expr, expressions),
- IrExpression::RestoreView(rv) => {
- if let crate::ir::expression::RestoreViewTarget::Dynamic(e) = &rv.view {
- expression_contains_context(e, expressions)
- } else {
- false
- }
- }
- // Leaf expressions
- _ => false,
- }
-}
-
-/// Check if an Angular AST expression contains any reference to the implicit receiver (this).
-/// This includes property reads like `this.foo` and method calls like `this.bar()`.
-fn ast_contains_implicit_receiver(ast: &crate::ast::expression::AngularExpression<'_>) -> bool {
- use crate::ast::expression::AngularExpression;
-
- match ast {
- // Direct implicit receiver reference
- AngularExpression::ImplicitReceiver(_) => true,
- // Property read - check if it's on implicit receiver or recurse
- AngularExpression::PropertyRead(pr) => ast_contains_implicit_receiver(&pr.receiver),
- // Safe property read
- AngularExpression::SafePropertyRead(pr) => ast_contains_implicit_receiver(&pr.receiver),
- // Keyed read
- AngularExpression::KeyedRead(kr) => {
- ast_contains_implicit_receiver(&kr.receiver) || ast_contains_implicit_receiver(&kr.key)
- }
- // Safe keyed read
- AngularExpression::SafeKeyedRead(kr) => {
- ast_contains_implicit_receiver(&kr.receiver) || ast_contains_implicit_receiver(&kr.key)
- }
- // Function call
- AngularExpression::Call(call) => {
- ast_contains_implicit_receiver(&call.receiver)
- || call.args.iter().any(ast_contains_implicit_receiver)
- }
- // Safe call
- AngularExpression::SafeCall(call) => {
- ast_contains_implicit_receiver(&call.receiver)
- || call.args.iter().any(ast_contains_implicit_receiver)
- }
- // Binary expression
- AngularExpression::Binary(b) => {
- ast_contains_implicit_receiver(&b.left) || ast_contains_implicit_receiver(&b.right)
- }
- // Unary expression
- AngularExpression::Unary(u) => ast_contains_implicit_receiver(&u.expr),
- // Conditional (ternary)
- AngularExpression::Conditional(c) => {
- ast_contains_implicit_receiver(&c.condition)
- || ast_contains_implicit_receiver(&c.true_exp)
- || ast_contains_implicit_receiver(&c.false_exp)
- }
- // Pipe binding
- AngularExpression::BindingPipe(p) => {
- ast_contains_implicit_receiver(&p.exp)
- || p.args.iter().any(ast_contains_implicit_receiver)
- }
- // Not expressions
- AngularExpression::PrefixNot(n) => ast_contains_implicit_receiver(&n.expression),
- AngularExpression::NonNullAssert(n) => ast_contains_implicit_receiver(&n.expression),
- // Typeof/void expressions
- AngularExpression::TypeofExpression(t) => ast_contains_implicit_receiver(&t.expression),
- AngularExpression::VoidExpression(v) => ast_contains_implicit_receiver(&v.expression),
- AngularExpression::SpreadElement(spread) => {
- ast_contains_implicit_receiver(&spread.expression)
- }
- // Chain - check all expressions
- AngularExpression::Chain(c) => c.expressions.iter().any(ast_contains_implicit_receiver),
- // Interpolation - check all expressions
- AngularExpression::Interpolation(i) => {
- i.expressions.iter().any(ast_contains_implicit_receiver)
- }
- // Template literals
- AngularExpression::TemplateLiteral(t) => {
- t.expressions.iter().any(ast_contains_implicit_receiver)
- }
- AngularExpression::TaggedTemplateLiteral(t) => {
- ast_contains_implicit_receiver(&t.tag)
- || t.template.expressions.iter().any(ast_contains_implicit_receiver)
- }
- // Array literal
- AngularExpression::LiteralArray(arr) => {
- arr.expressions.iter().any(ast_contains_implicit_receiver)
- }
- // Map literal
- AngularExpression::LiteralMap(map) => map.values.iter().any(ast_contains_implicit_receiver),
- // Parenthesized expression
- AngularExpression::ParenthesizedExpression(p) => {
- ast_contains_implicit_receiver(&p.expression)
- }
- // Arrow function - check the body
- AngularExpression::ArrowFunction(arrow) => ast_contains_implicit_receiver(&arrow.body),
- // Literals and other leaf nodes don't contain implicit receiver
- AngularExpression::LiteralPrimitive(_)
- | AngularExpression::Empty(_)
- | AngularExpression::ThisReceiver(_)
- | AngularExpression::RegularExpressionLiteral(_) => false,
- }
-}
-
/// Check if the track expression is a simple variable read of $index or $item.
fn check_simple_track_variable(
track: &IrExpression<'_>,
diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs
index 4be2806dc..3cbb514cc 100644
--- a/crates/oxc_angular_compiler/tests/integration_test.rs
+++ b/crates/oxc_angular_compiler/tests/integration_test.rs
@@ -1188,6 +1188,130 @@ fn test_nested_for_with_outer_scope_track() {
insta::assert_snapshot!("nested_for_with_outer_scope_track", js);
}
+/// Tests that `track prefix() + item.id` generates a regular function (not arrow function).
+/// When a binary expression in track contains a component method call, the generated
+/// track function must use `function` declaration to properly bind `this`.
+#[test]
+fn test_for_track_binary_with_component_method() {
+ let js = compile_template_to_js(
+ r#"@for (item of items; track prefix() + item.id) {
{{item.name}}
}"#,
+ "TestComponent",
+ );
+ // Must generate a regular function, not an arrow function, because prefix() needs `this`
+ assert!(
+ js.contains("function _forTrack"),
+ "Track with binary operator containing component method should generate a regular function. Output:\n{js}"
+ );
+ assert!(
+ js.contains("this.prefix()"),
+ "Track function should use 'this.prefix()' for component method access. Output:\n{js}"
+ );
+ // Must NOT be an arrow function (arrow functions don't bind `this`)
+ assert!(
+ !js.contains("const _forTrack"),
+ "Should NOT generate an arrow function (const _forTrack = ...) for track expressions that reference component members. Output:\n{js}"
+ );
+ insta::assert_snapshot!("for_track_binary_with_component_method", js);
+}
+
+/// Tests that nullish coalescing (??) in track with component method generates a regular function.
+/// This is the exact pattern from the original bug report: `track item.prefix ?? defaultPrefix()`
+#[test]
+fn test_for_track_nullish_coalescing_with_component_method() {
+ let js = compile_template_to_js(
+ r#"@for (item of items; track item.prefix ?? defaultPrefix()) { {{item.name}}
}"#,
+ "TestComponent",
+ );
+ assert!(
+ js.contains("function _forTrack"),
+ "Track with ?? operator containing component method should generate a regular function. Output:\n{js}"
+ );
+ assert!(
+ !js.contains("const _forTrack"),
+ "Should NOT generate an arrow function for track with ?? referencing component members. Output:\n{js}"
+ );
+ insta::assert_snapshot!("for_track_nullish_coalescing_with_component_method", js);
+}
+
+/// Tests that ternary in track with component method generates a regular function.
+#[test]
+fn test_for_track_ternary_with_component_method() {
+ let js = compile_template_to_js(
+ r#"@for (item of items; track useId() ? item.id : item.name) { {{item.name}}
}"#,
+ "TestComponent",
+ );
+ assert!(
+ js.contains("function _forTrack"),
+ "Track with ternary containing component method should generate a regular function. Output:\n{js}"
+ );
+ assert!(
+ !js.contains("const _forTrack"),
+ "Should NOT generate an arrow function for track with ternary referencing component members. Output:\n{js}"
+ );
+ insta::assert_snapshot!("for_track_ternary_with_component_method", js);
+}
+
+/// Tests that a complex track expression with multiple component references and binary operators
+/// generates a regular function. Mirrors the original bug: `(tag.queryPrefix ?? queryPrefix()) + '.' + tag.key`
+#[test]
+fn test_for_track_complex_binary_with_nullish_coalescing() {
+ let js = compile_template_to_js(
+ r#"@for (tag of visibleTags(); track (tag.queryPrefix ?? queryPrefix()) + '.' + tag.key) { {{ tag.key }} }"#,
+ "TestComponent",
+ );
+ assert!(
+ js.contains("function _forTrack"),
+ "Complex track with ?? and + containing component method should generate a regular function. Output:\n{js}"
+ );
+ assert!(
+ !js.contains("const _forTrack"),
+ "Should NOT generate an arrow function. Output:\n{js}"
+ );
+ assert!(
+ js.contains("this.queryPrefix()"),
+ "Track function should use 'this.queryPrefix()' for component method access. Output:\n{js}"
+ );
+ insta::assert_snapshot!("for_track_complex_binary_with_nullish_coalescing", js);
+}
+
+/// Tests that a track expression with only item property reads in binary operators
+/// correctly generates an arrow function (no component context needed).
+#[test]
+fn test_for_track_binary_without_component_context() {
+ let js = compile_template_to_js(
+ r#"@for (item of items; track item.type + ':' + item.id) { {{item.name}}
}"#,
+ "TestComponent",
+ );
+ // This should be an arrow function since no component members are referenced
+ assert!(
+ js.contains("const _forTrack"),
+ "Track with binary operator using only item properties should generate an arrow function. Output:\n{js}"
+ );
+ assert!(
+ !js.contains("function _forTrack"),
+ "Should NOT generate a regular function when no component members are referenced. Output:\n{js}"
+ );
+ insta::assert_snapshot!("for_track_binary_without_component_context", js);
+}
+
+/// Tests that negation (!) in track with component method generates a regular function.
+#[test]
+fn test_for_track_not_with_component_method() {
+ let js = compile_template_to_js(
+ r#"@for (item of items; track !isDisabled()) { {{item.name}}
}"#,
+ "TestComponent",
+ );
+ assert!(
+ js.contains("function _forTrack"),
+ "Track with ! operator containing component method should generate a regular function. Output:\n{js}"
+ );
+ assert!(
+ !js.contains("const _forTrack"),
+ "Should NOT generate an arrow function. Output:\n{js}"
+ );
+ insta::assert_snapshot!("for_track_not_with_component_method", js);
+}
+
#[test]
fn test_if_inside_for() {
let js = compile_template_to_js(
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_binary_with_component_method.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_binary_with_component_method.snap
new file mode 100644
index 000000000..69e623c19
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_binary_with_component_method.snap
@@ -0,0 +1,26 @@
+---
+source: crates/oxc_angular_compiler/tests/integration_test.rs
+expression: js
+---
+function _forTrack0($index,$item) {
+ return (this.prefix() + $item.id);
+}
+function TestComponent_For_1_Template(rf,ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵtext(0," ");
+ i0.ɵɵelementStart(1,"div");
+ i0.ɵɵtext(2);
+ i0.ɵɵelementEnd();
+ i0.ɵɵtext(3," ");
+ }
+ if ((rf & 2)) {
+ const item_r1 = ctx.$implicit;
+ i0.ɵɵadvance(2);
+ i0.ɵɵtextInterpolate(item_r1.name);
+ }
+}
+function TestComponent_Template(rf,ctx) {
+ if ((rf & 1)) { i0.ɵɵrepeaterCreate(0,TestComponent_For_1_Template,4,1,null,null,_forTrack0,
+ true); }
+ if ((rf & 2)) { i0.ɵɵrepeater(ctx.items); }
+}
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_binary_without_component_context.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_binary_without_component_context.snap
new file mode 100644
index 000000000..d8039f349
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_binary_without_component_context.snap
@@ -0,0 +1,23 @@
+---
+source: crates/oxc_angular_compiler/tests/integration_test.rs
+expression: js
+---
+const _forTrack0 = ($index,$item) =>(($item.type + ":") + $item.id);
+function TestComponent_For_1_Template(rf,ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵtext(0," ");
+ i0.ɵɵelementStart(1,"div");
+ i0.ɵɵtext(2);
+ i0.ɵɵelementEnd();
+ i0.ɵɵtext(3," ");
+ }
+ if ((rf & 2)) {
+ const item_r1 = ctx.$implicit;
+ i0.ɵɵadvance(2);
+ i0.ɵɵtextInterpolate(item_r1.name);
+ }
+}
+function TestComponent_Template(rf,ctx) {
+ if ((rf & 1)) { i0.ɵɵrepeaterCreate(0,TestComponent_For_1_Template,4,1,null,null,_forTrack0); }
+ if ((rf & 2)) { i0.ɵɵrepeater(ctx.items); }
+}
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_complex_binary_with_nullish_coalescing.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_complex_binary_with_nullish_coalescing.snap
new file mode 100644
index 000000000..56818eae3
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_complex_binary_with_nullish_coalescing.snap
@@ -0,0 +1,26 @@
+---
+source: crates/oxc_angular_compiler/tests/integration_test.rs
+expression: js
+---
+function _forTrack0($index,$item) {
+ return ((($item.queryPrefix ?? this.queryPrefix()) + ".") + $item.key);
+}
+function TestComponent_For_1_Template(rf,ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵtext(0," ");
+ i0.ɵɵelementStart(1,"span");
+ i0.ɵɵtext(2);
+ i0.ɵɵelementEnd();
+ i0.ɵɵtext(3," ");
+ }
+ if ((rf & 2)) {
+ const tag_r1 = ctx.$implicit;
+ i0.ɵɵadvance(2);
+ i0.ɵɵtextInterpolate(tag_r1.key);
+ }
+}
+function TestComponent_Template(rf,ctx) {
+ if ((rf & 1)) { i0.ɵɵrepeaterCreate(0,TestComponent_For_1_Template,4,1,null,null,_forTrack0,
+ true); }
+ if ((rf & 2)) { i0.ɵɵrepeater(ctx.visibleTags()); }
+}
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_not_with_component_method.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_not_with_component_method.snap
new file mode 100644
index 000000000..afced8690
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_not_with_component_method.snap
@@ -0,0 +1,26 @@
+---
+source: crates/oxc_angular_compiler/tests/integration_test.rs
+expression: js
+---
+function _forTrack0($index,$item) {
+ return !this.isDisabled();
+}
+function TestComponent_For_1_Template(rf,ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵtext(0," ");
+ i0.ɵɵelementStart(1,"div");
+ i0.ɵɵtext(2);
+ i0.ɵɵelementEnd();
+ i0.ɵɵtext(3," ");
+ }
+ if ((rf & 2)) {
+ const item_r1 = ctx.$implicit;
+ i0.ɵɵadvance(2);
+ i0.ɵɵtextInterpolate(item_r1.name);
+ }
+}
+function TestComponent_Template(rf,ctx) {
+ if ((rf & 1)) { i0.ɵɵrepeaterCreate(0,TestComponent_For_1_Template,4,1,null,null,_forTrack0,
+ true); }
+ if ((rf & 2)) { i0.ɵɵrepeater(ctx.items); }
+}
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_nullish_coalescing_with_component_method.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_nullish_coalescing_with_component_method.snap
new file mode 100644
index 000000000..a182134fe
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_nullish_coalescing_with_component_method.snap
@@ -0,0 +1,26 @@
+---
+source: crates/oxc_angular_compiler/tests/integration_test.rs
+expression: js
+---
+function _forTrack0($index,$item) {
+ return ($item.prefix ?? this.defaultPrefix());
+}
+function TestComponent_For_1_Template(rf,ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵtext(0," ");
+ i0.ɵɵelementStart(1,"div");
+ i0.ɵɵtext(2);
+ i0.ɵɵelementEnd();
+ i0.ɵɵtext(3," ");
+ }
+ if ((rf & 2)) {
+ const item_r1 = ctx.$implicit;
+ i0.ɵɵadvance(2);
+ i0.ɵɵtextInterpolate(item_r1.name);
+ }
+}
+function TestComponent_Template(rf,ctx) {
+ if ((rf & 1)) { i0.ɵɵrepeaterCreate(0,TestComponent_For_1_Template,4,1,null,null,_forTrack0,
+ true); }
+ if ((rf & 2)) { i0.ɵɵrepeater(ctx.items); }
+}
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_ternary_with_component_method.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_ternary_with_component_method.snap
new file mode 100644
index 000000000..5bc8a2caf
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_track_ternary_with_component_method.snap
@@ -0,0 +1,26 @@
+---
+source: crates/oxc_angular_compiler/tests/integration_test.rs
+expression: js
+---
+function _forTrack0($index,$item) {
+ return (this.useId()? $item.id: $item.name);
+}
+function TestComponent_For_1_Template(rf,ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵtext(0," ");
+ i0.ɵɵelementStart(1,"div");
+ i0.ɵɵtext(2);
+ i0.ɵɵelementEnd();
+ i0.ɵɵtext(3," ");
+ }
+ if ((rf & 2)) {
+ const item_r1 = ctx.$implicit;
+ i0.ɵɵadvance(2);
+ i0.ɵɵtextInterpolate(item_r1.name);
+ }
+}
+function TestComponent_Template(rf,ctx) {
+ if ((rf & 1)) { i0.ɵɵrepeaterCreate(0,TestComponent_For_1_Template,4,1,null,null,_forTrack0,
+ true); }
+ if ((rf & 2)) { i0.ɵɵrepeater(ctx.items); }
+}
diff --git a/napi/angular-compiler/e2e/compare/fixtures/control-flow/for-track-variations.fixture.ts b/napi/angular-compiler/e2e/compare/fixtures/control-flow/for-track-variations.fixture.ts
index 2b1ed1d88..4ab2b7dc8 100644
--- a/napi/angular-compiler/e2e/compare/fixtures/control-flow/for-track-variations.fixture.ts
+++ b/napi/angular-compiler/e2e/compare/fixtures/control-flow/for-track-variations.fixture.ts
@@ -102,6 +102,81 @@ export class ForWithEmptyComponent {
`.trim(),
expectedFeatures: ['ɵɵrepeaterCreate', 'ɵɵrepeaterTrackByIdentity'],
},
+ {
+ name: 'for-track-binary-with-component-method',
+ category: 'control-flow',
+ description: '@for with track using binary operator and component method',
+ className: 'ForTrackBinaryComponentMethodComponent',
+ type: 'full-transform',
+ sourceCode: `
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-for-track-binary-method',
+ standalone: true,
+ template: \`
+ @for (item of items; track prefix() + item.id) {
+ {{ item.name }}
+ }
+ \`,
+})
+export class ForTrackBinaryComponentMethodComponent {
+ items = [{ id: '1', name: 'Item 1' }];
+ prefix() { return 'pfx'; }
+}
+ `.trim(),
+ expectedFeatures: ['ɵɵrepeaterCreate'],
+ },
+ {
+ name: 'for-track-nullish-coalescing-with-component-method',
+ category: 'control-flow',
+ description: '@for with track using ?? operator and component method',
+ className: 'ForTrackNullishCoalesceComponent',
+ type: 'full-transform',
+ sourceCode: `
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-for-track-nullish',
+ standalone: true,
+ template: \`
+ @for (tag of tags; track (tag.queryPrefix ?? queryPrefix()) + '.' + tag.key) {
+ {{ tag.key }}
+ }
+ \`,
+})
+export class ForTrackNullishCoalesceComponent {
+ tags = [{ queryPrefix: null, key: 'k1' }];
+ queryPrefix() { return 'default'; }
+}
+ `.trim(),
+ expectedFeatures: ['ɵɵrepeaterCreate'],
+ },
+ {
+ name: 'for-track-ternary-with-component-method',
+ category: 'control-flow',
+ description: '@for with track using ternary and component method',
+ className: 'ForTrackTernaryComponentMethodComponent',
+ type: 'full-transform',
+ sourceCode: `
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-for-track-ternary-method',
+ standalone: true,
+ template: \`
+ @for (item of items; track useId() ? item.id : item.name) {
+ {{ item.name }}
+ }
+ \`,
+})
+export class ForTrackTernaryComponentMethodComponent {
+ items = [{ id: '1', name: 'Item 1' }];
+ useId() { return true; }
+}
+ `.trim(),
+ expectedFeatures: ['ɵɵrepeaterCreate'],
+ },
{
name: 'for-context-variables',
category: 'control-flow',