Skip to content

Commit 60277ce

Browse files
authored
test(compiler): cover @Inject(TOKEN) on pipe constructor params (#235)
1 parent b2dd390 commit 60277ce

File tree

1 file changed

+129
-0
lines changed

1 file changed

+129
-0
lines changed

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9312,3 +9312,132 @@ export class MyComponent {}
93129312
result.code
93139313
);
93149314
}
9315+
9316+
// =============================================================================
9317+
// Regression: @Inject(TOKEN) on pipe constructor parameters
9318+
// =============================================================================
9319+
// The `extract_param_dependency` function in `pipe/decorator.rs` previously did
9320+
// not handle the `@Inject(TOKEN)` decorator, so the token was silently taken
9321+
// from the TypeScript type annotation instead. When the type was an interface
9322+
// (erased at runtime) this left the DI token undefined, which Angular 20's
9323+
// `assertDefined(token)` guard rejects immediately. See commit b2dd390.
9324+
9325+
/// `@Inject(TOKEN)` on a pipe constructor param must produce a factory that
9326+
/// injects the TOKEN identifier, not the erased type annotation.
9327+
#[test]
9328+
fn test_pipe_factory_uses_inject_token_over_interface_type() {
9329+
let allocator = Allocator::default();
9330+
let source = r"
9331+
import { Pipe, PipeTransform, Inject, InjectionToken } from '@angular/core';
9332+
9333+
export interface Config {
9334+
locale: string;
9335+
}
9336+
9337+
export const CONFIG = new InjectionToken<Config>('CONFIG');
9338+
9339+
@Pipe({ name: 'localized', standalone: true })
9340+
export class LocalizedPipe implements PipeTransform {
9341+
constructor(@Inject(CONFIG) private config: Config) {}
9342+
transform(value: string): string { return value; }
9343+
}
9344+
";
9345+
9346+
let result = transform_angular_file(&allocator, "localized.pipe.ts", source, None, None);
9347+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
9348+
9349+
let code = &result.code;
9350+
let factory_section =
9351+
code.split("ɵfac").nth(1).expect("Should have a factory definition (ɵfac)");
9352+
9353+
// Factory must inject CONFIG (the @Inject token), not Config (the interface type).
9354+
assert!(
9355+
factory_section.contains("CONFIG"),
9356+
"Pipe factory should inject the @Inject(CONFIG) token. Factory:\n{factory_section}"
9357+
);
9358+
assert!(
9359+
!factory_section.contains("directiveInject(Config)")
9360+
&& !factory_section.contains("ɵɵinject(Config)"),
9361+
"Pipe factory must NOT inject the erased interface type 'Config'. Factory:\n{factory_section}"
9362+
);
9363+
}
9364+
9365+
/// When `@Inject(TOKEN)` is used alongside modifier decorators (`@Optional`,
9366+
/// `@SkipSelf`), the factory must still pick up the TOKEN and forward the
9367+
/// correct DI flags.
9368+
#[test]
9369+
fn test_pipe_factory_inject_token_with_optional_skip_self() {
9370+
let allocator = Allocator::default();
9371+
let source = r"
9372+
import { Pipe, PipeTransform, Inject, Optional, SkipSelf, InjectionToken } from '@angular/core';
9373+
9374+
export const MY_TOKEN = new InjectionToken<string>('MY_TOKEN');
9375+
9376+
@Pipe({ name: 'tagged', standalone: true })
9377+
export class TaggedPipe implements PipeTransform {
9378+
constructor(
9379+
@Optional() @SkipSelf() @Inject(MY_TOKEN) private tag: string | null,
9380+
) {}
9381+
transform(value: string): string { return value; }
9382+
}
9383+
";
9384+
9385+
let result = transform_angular_file(&allocator, "tagged.pipe.ts", source, None, None);
9386+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
9387+
9388+
let code = &result.code;
9389+
let factory_section =
9390+
code.split("ɵfac").nth(1).expect("Should have a factory definition (ɵfac)");
9391+
9392+
// MY_TOKEN must be present in the factory as the DI token.
9393+
assert!(
9394+
factory_section.contains("directiveInject(MY_TOKEN"),
9395+
"Pipe factory should inject MY_TOKEN. Factory:\n{factory_section}"
9396+
);
9397+
9398+
// The DI flag bitmask must include Optional (8) | SkipSelf (4). Angular's
9399+
// pipe compilation also ORs in ForPipe (16), yielding 28. We only require
9400+
// that both Optional and SkipSelf bits are set.
9401+
let flags = factory_section
9402+
.split("directiveInject(MY_TOKEN,")
9403+
.nth(1)
9404+
.and_then(|s| s.split(')').next())
9405+
.and_then(|s| s.trim().parse::<u32>().ok())
9406+
.expect("Factory should encode numeric DI flags");
9407+
assert!(
9408+
flags & 8 != 0 && flags & 4 != 0,
9409+
"Pipe factory flags should include Optional (8) and SkipSelf (4). Got: {flags}. Factory:\n{factory_section}"
9410+
);
9411+
}
9412+
9413+
/// Without `@Inject`, the factory must still fall back to the type annotation
9414+
/// so that plain class-typed dependencies continue to resolve correctly.
9415+
#[test]
9416+
fn test_pipe_factory_without_inject_still_uses_type_annotation() {
9417+
let allocator = Allocator::default();
9418+
let source = r"
9419+
import { Pipe, PipeTransform } from '@angular/core';
9420+
9421+
export class Logger {
9422+
log(msg: string): void {}
9423+
}
9424+
9425+
@Pipe({ name: 'logged', standalone: true })
9426+
export class LoggedPipe implements PipeTransform {
9427+
constructor(private logger: Logger) {}
9428+
transform(value: string): string { return value; }
9429+
}
9430+
";
9431+
9432+
let result = transform_angular_file(&allocator, "logged.pipe.ts", source, None, None);
9433+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
9434+
9435+
let code = &result.code;
9436+
let factory_section =
9437+
code.split("ɵfac").nth(1).expect("Should have a factory definition (ɵfac)");
9438+
9439+
assert!(
9440+
factory_section.contains("Logger"),
9441+
"Pipe factory should fall back to the type annotation (Logger). Factory:\n{factory_section}"
9442+
);
9443+
}

0 commit comments

Comments
 (0)