1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Anuncie Aqui
    Anuncie aqui você Também: fdantas@4each.com.br

Error in unit test after implementing ngx-translate: TypeError: Cannot read properties of...

Discussão em 'Angular' iniciado por Miguel Frias, Agosto 26, 2025.

  1. Miguel Frias

    Miguel Frias Guest

    Currently im working with angular 20, and i just implemented ngx-translate, since that moment all my unit test started falling with this two issues:

    ToolDialogComponent › should create

    TypeError: Cannot read properties of undefined (reading 'subscribe')


    and

    ToolDialogComponent › should create

    1 component threw errors during cleanup


    this is the current unit test:

    describe('ToolDialogComponent', () => {
    let fixture: ComponentFixture<ToolDialogComponent>;
    let component: ToolDialogComponent;
    let injector: Injector;
    let createdComponents: ComponentFixture<ToolDialogComponent>[] = [];

    beforeEach(async () => {
    await TestBed.configureTestingModule({
    imports: [ToolDialogComponent, ReactiveFormsModule],
    providers: [
    provideMockStore({
    initialState: {
    aiAgent: {
    explicitTools: {},
    },
    },
    }),
    {
    provide: TranslateService,
    useClass: TranslateMockService,
    },
    {
    provide: DialogRef,
    useFactory: () => new MockDialogRef(DEFAULT_DIALOG_DATA), // always provide a valid instance
    },
    ],
    })
    .overrideComponent(ToolDialogComponent, {
    set: {
    imports: [
    AvatarComponent,
    TitleComponent,
    CdkScrollable,
    CommonModule,
    ScrollbarDirective,
    BarModule,
    DialogFooterComponent,
    DialogBodyComponent,
    DialogHeaderComponent,
    DialogComponent,
    ContentDensityDirective,
    FormModule,
    FormsModule,
    ReactiveFormsModule,
    MessageStripModule,
    JouleSkillComponent,
    TranslateMockPipe,
    DocumentResourcesComponent,
    TrimFormControlDirective,
    ],
    },
    })
    .compileComponents();

    injector = TestBed.inject(Injector);
    const store = TestBed.inject(MockStore);
    store.overrideSelector(selectJouleSkills, mockedJouleSkills);
    store.overrideSelector(selectAvailableJouleSkills, mockedAvailableJouleSkills);
    store.overrideSelector(selectGetJouleSkillsRequested, false);
    store.overrideSelector(selectAiAgentExplicitTools, mockedAiAgentExplicitTools);
    });

    function createComponentWithDialogData(data: ToolDialogInput = DEFAULT_DIALOG_DATA) {
    fixture = TestBed.createComponent(ToolDialogComponent);
    component = fixture.componentInstance;

    const dialogRef = new MockDialogRef(data);
    (component as any)['dialogRef'] = dialogRef;

    fixture.detectChanges();

    createdComponents.push(fixture);
    return { component, dialogRef, fixture };
    }

    afterEach(() => {
    createdComponents.forEach((f) => f.destroy());
    createdComponents = [];
    });

    it('should create', () => {
    ({ component } = createComponentWithDialogData());
    expect(component).toBeTruthy();
    });

    // ---------------- updateTool ----------------
    describe('updateTool', () => {
    it('should close dialogRef in add mode', () => {
    ({ component, dialogRef } = createComponentWithDialogData());
    component.form.patchValue({ name: 'aName', description: 'aDescription' });
    component.updateTool();

    expect(dialogRef.close).toHaveBeenCalledWith({
    tools: {
    [ToolType.CALCULATOR]: {
    type: `$${ToolProvider.PAB}.${ToolType.CALCULATOR}`,
    name: 'aName',
    description: 'aDescription',
    callableElementRef: '',
    },
    },
    });
    });

    it('should return correct response when calculator type is passed in add mode', () => {
    ({ component, dialogRef } = createComponentWithDialogData());
    component.form.patchValue({ name: 'aName added', description: 'aDescription added' });
    component.updateTool();

    expect(dialogRef.close).toHaveBeenCalledWith({
    tools: {
    [ToolType.CALCULATOR]: {
    type: `$${ToolProvider.PAB}.${ToolType.CALCULATOR}`,
    name: 'aName added',
    description: 'aDescription added',
    callableElementRef: '',
    },
    },
    });
    });

    it('should correctly set name and description in edit mode', () => {
    const editData: ToolDialogInput = {
    editMode: true,
    toolType: ToolType.DOCUMENT,
    toolKey: ToolType.DOCUMENT,
    explicitTool: {
    type: `$${ToolProvider.PAB}.${ToolType.DOCUMENT}`,
    name: 'aName',
    description: 'aDescription',
    callableElementRef: '',
    },
    };
    ({ component, dialogRef } = createComponentWithDialogData(editData));
    component.form.patchValue({ name: 'aName edited', description: 'aDescription edited' });
    component.updateTool();

    expect(dialogRef.close).toHaveBeenCalledWith({
    tools: {
    [ToolType.DOCUMENT]: {
    type: `$${ToolProvider.PAB}.${ToolType.DOCUMENT}`,
    name: 'aName edited',
    description: 'aDescription edited',
    callableElementRef: '',
    configuration: {
    destinationName: '',
    resourceGroupId: '',
    collectionIds: [],
    },
    },
    },
    });
    });
    });

    // ---------------- addSelectedJouleSkills ----------------
    describe('addJouleSkillTool', () => {
    it('should return correct response when addJouleSkillTool is called', () => {
    ({ component, dialogRef } = createComponentWithDialogData({
    editMode: false,
    toolType: ToolType.JOULE_SKILL,
    }));
    const dummyJouleSkill: JouleSkillTool = {
    uid: '123',
    identifier: 'firstskill',
    name: 'Test Tool',
    description: 'Test Description',
    };
    component.onSelectedJouleSkillsChange([dummyJouleSkill]);
    component.addSelectedJouleSkills();

    expect(dialogRef.close).toHaveBeenCalledWith({
    tools: {
    [`${ToolType.JOULE_SKILL}_firstskill`]: {
    type: `$${ToolProvider.SBPA}.${ToolType.JOULE_SKILL}`,
    name: 'Test Tool',
    description: 'Test Description',
    callableElementRef: 'firstskill.123',
    },
    },
    });
    });
    });

    // ---------------- addSelectedJouleSkillsDisabled ----------------
    describe('addSelectedJouleSkillsDisabled', () => {
    it('should return true if no selectedJouleSkills', () => {
    ({ component } = createComponentWithDialogData({ editMode: false, toolType: ToolType.JOULE_SKILL }));
    component.onSelectedJouleSkillsChange([]);
    expect(component.addSelectedJouleSkillsDisabled()).toBe(true);
    });

    it('should return true if max allowed tools exceeded', () => {
    ({ component } = createComponentWithDialogData({ editMode: false, toolType: ToolType.JOULE_SKILL }));
    component.onSelectedJouleSkillsChange([mockedJouleSkills[0], mockedJouleSkills[1]]);
    (component as any).aiAgentExplicitTools.set({
    existing1: {
    type: `$${ToolProvider.SBPA}.${ToolType.JOULE_SKILL}`,
    name: '',
    description: '',
    callableElementRef: '',
    },
    existing2: {
    type: `$${ToolProvider.SBPA}.${ToolType.JOULE_SKILL}`,
    name: '',
    description: '',
    callableElementRef: '',
    },
    });
    expect(component.addSelectedJouleSkillsDisabled()).toBe(true);
    });

    it('should return false if selectedJouleSkills are within limit', () => {
    ({ component } = createComponentWithDialogData({ editMode: false, toolType: ToolType.JOULE_SKILL }));
    component.onSelectedJouleSkillsChange([mockedJouleSkills[0]]);
    (component as any).aiAgentExplicitTools.set({});
    expect(component.addSelectedJouleSkillsDisabled()).toBe(false);
    });
    });

    // ---------------- updateDocumentResources ----------------
    describe('updateDocumentResources', () => {
    it('should update documentResourcesFormOutput and affect updateTool behavior', () => {
    const editData: ToolDialogInput = {
    editMode: true,
    toolType: ToolType.DOCUMENT,
    toolKey: ToolType.DOCUMENT,
    explicitTool: {
    type: `$${ToolProvider.PAB}.${ToolType.DOCUMENT}`,
    name: 'aName',
    description: 'aDescription',
    callableElementRef: '',
    },
    };
    ({ component, dialogRef } = createComponentWithDialogData(editData));

    const mockOutput = {
    data: {
    destinationName: 'Test Destination',
    resourceGroupId: 'Test Group',
    collectionIds: ['Test Collection'],
    },
    errors: undefined,
    };

    component.updateDocumentResourcesFormOutput(mockOutput);
    component.form.patchValue({ name: 'Updated Name', description: 'Updated Description' });
    component.updateTool();

    expect(dialogRef.close).toHaveBeenCalledWith({
    tools: {
    [ToolType.DOCUMENT]: {
    type: `$${ToolProvider.PAB}.${ToolType.DOCUMENT}`,
    name: 'Updated Name',
    description: 'Updated Description',
    callableElementRef: '',
    configuration: {
    destinationName: 'Test Destination',
    resourceGroupId: 'Test Group',
    collectionIds: ['Test Collection'],
    },
    },
    },
    });
    });
    });
    });


    this is the component:

    export class ToolDialogComponent implements OnInit {
    private readonly dialogRef = inject(DialogRef<ToolDialogInput, ToolDialogOutput>);
    private readonly store = inject(Store);
    private readonly translateService = inject(TranslateService);

    private readonly aiAgentExplicitTools = this.store.selectSignal(selectAiAgentExplicitTools);

    private readonly documentResourcesFormOutput = signal<DocumentResourcesFormOutput | undefined>(undefined);
    private readonly dialogData = signal(this.dialogRef.data);
    private readonly toolType = computed(() => this.dialogData().toolType);

    readonly selectedJouleSkills = signal<JouleSkillTool[]>([]);

    readonly editMode = computed(() => this.dialogData().editMode);

    readonly toolIcon = computed(() => ToolTypeMapping[this.toolType()].icon ?? 'wrench');
    readonly toolColor = computed(() => ToolTypeMapping[this.toolType()].color ?? 10);

    readonly dialogTitle = rxResource({
    request: () => this.dialogData(),
    loader: ({ request }) =>
    request.editMode
    ? this.translateService.get('aiAgentToolDialogEditToolTitle.text')
    : this.translateService.get('aiAgentToolDialogAddToolTitle.text'),
    });

    readonly isDocumentToolType = computed(() => this.toolType() === ToolType.DOCUMENT);
    readonly isJouleSkillToolType = computed(() => this.toolType() === ToolType.JOULE_SKILL);

    readonly referencedJouleSkill = computed(() => this.fetchReferencedJouleSkill(this.dialogData()));

    readonly documentToolConfig = signal<AIAgentDocumentToolConfig | undefined>(undefined);

    readonly documentResourceFormHasErrors = computed(() => {
    const documentResourcesOutput = this.documentResourcesFormOutput();
    return documentResourcesOutput?.errors && Object.keys(documentResourcesOutput.errors).length > 0;
    });

    readonly addSelectedJouleSkillsDisabled = computed(() => {
    const selectedCount = this.selectedJouleSkills().length;
    const existingCount = Object.values(this.aiAgentExplicitTools() ?? {}).filter(
    (item) => getToolTypeFromComposed(item.type) === ToolType.JOULE_SKILL,
    ).length;
    return !selectedCount || existingCount + selectedCount > MAX_AGENT_TOOLS_BY_TYPE[ToolType.JOULE_SKILL];
    });

    readonly NAME_MAX_LENGTH = 64;
    readonly DESCRIPTION_MAX_LENGTH = 1024;

    readonly form = new FormGroup({
    name: new FormControl<string>('', {
    nonNullable: true,
    validators: [Validators.required, Validators.maxLength(this.NAME_MAX_LENGTH)],
    }),
    description: new FormControl<string>('', {
    nonNullable: true,
    validators: [Validators.required, Validators.maxLength(this.DESCRIPTION_MAX_LENGTH)],
    }),
    });

    ngOnInit() {
    this.setFormValues();
    }

    get isToolNameInvalid() {
    return isFormControlInvalid(this.form.get('name'));
    }

    get isToolNameLengthInvalid() {
    return isFormControlLengthInvalid(this.form.get('name'));
    }

    get isToolDescriptionInvalid() {
    return isFormControlInvalid(this.form.get('description'));
    }

    get isToolDescriptionLengthInvalid() {
    return isFormControlLengthInvalid(this.form.get('description'));
    }

    updateTool() {
    const documentResourceData = this.documentResourcesFormOutput()?.data;

    const configuration = this.isDocumentToolType() && documentResourceData ? documentResourceData : undefined;

    const { name, description } = this.form.value;

    if (this.editMode()) {
    const explicitTool = this.dialogRef.data.explicitTool;
    const toolKey = this.dialogData().toolKey;
    if (!explicitTool || !toolKey) {
    return;
    }

    const tool: AIAgentExplicitTool = {
    ...explicitTool,
    name: name ?? '',
    description: description ?? '',
    configuration,
    };

    this.dialogRef.close({
    tools: { [toolKey]: tool },
    });
    } else {
    const toolType = this.toolType();
    const tool: AIAgentExplicitTool = {
    type: `$${ToolProvider.PAB}.${toolType}`,
    name: name ?? '',
    description: description ?? '',
    callableElementRef: '',
    configuration,
    };

    this.dialogRef.close({
    tools: { [toolType]: tool },
    });
    }
    }

    onSelectedJouleSkillsChange(jouleSkills: JouleSkillTool[]) {
    this.selectedJouleSkills.set(jouleSkills);
    }

    addSelectedJouleSkills() {
    const selectedJouleSkills = this.selectedJouleSkills();
    const tools: Record<string, AIAgentExplicitTool> = {};
    selectedJouleSkills.forEach((jouleSkill) => {
    const { toolKey, tool } = this.getExplicitTool(jouleSkill);
    tools[toolKey] = tool;
    });

    this.dialogRef.close({ tools });
    }

    closeDialog() {
    this.dialogRef.close();
    }

    updateDocumentResourcesFormOutput(output: DocumentResourcesFormOutput) {
    this.documentResourcesFormOutput.set(output);
    }

    private getExplicitTool(jouleSkill: JouleSkillTool): { toolKey: string; tool: AIAgentExplicitTool } {
    const toolKey = `${this.toolType()}_${jouleSkill.identifier}`;
    const tool: AIAgentExplicitTool = {
    type: `$${ToolProvider.SBPA}.${this.toolType()}`,
    name: jouleSkill.name,
    description: jouleSkill.description,
    callableElementRef: `${jouleSkill.identifier}.${jouleSkill.uid}`,
    };

    return { toolKey, tool };
    }

    private fetchReferencedJouleSkill(data: ToolDialogInput): JouleSkillTool | undefined {
    const { toolType, explicitTool } = data;
    if (!explicitTool || toolType !== ToolType.JOULE_SKILL) {
    return undefined;
    }
    const { callableElementRef } = explicitTool;
    const uid = callableElementRef.split('.')[1];

    return this.store.selectSignal(selectJouleSkillByUid(uid))();
    }

    private setFormValues() {
    const { explicitTool } = this.dialogRef.data;
    const { name, description, configuration } = explicitTool || {};

    if (this.isDocumentToolType() && !!configuration) {
    this.documentToolConfig.set(configuration);
    }

    this.form.patchValue({
    name: name ?? '',
    description: description ?? '',
    });
    }
    }


    to give more context this the mock i create to replace the TranslationModule:

    const translations: any = {};

    class LoaderMock implements TranslateLoader {
    getTranslation(lang: string): Observable<any> {
    return of(translations);
    }
    }

    @Pipe({ name: 'translate' })
    export class TranslateMockPipe implements PipeTransform {
    transform(value: string): string {
    return value; // just return the key directly
    }
    }

    export class TranslateMockService {
    onLangChange = new ReplaySubject<any>(1);
    onTranslationChange = new ReplaySubject<any>(1);
    onDefaultLangChange = new ReplaySubject<any>(1);

    defaultLang = 'en';

    constructor() {
    this.onLangChange.next({ lang: this.defaultLang });
    this.onTranslationChange.next({ lang: this.defaultLang, translations: {} });
    this.onDefaultLangChange.next({ lang: this.defaultLang });
    }

    get(key: string | string[]) {
    return of(key);
    }

    instant(key: string | string[]) {
    return key;
    }

    stream(key: string | string[]) {
    return of(key);
    }

    addLangs(_: string[]) {}

    // important: return an observable, not void
    use(lang: string): Observable<any> {
    this.defaultLang = lang;
    this.onLangChange.next({ lang });
    return of({ lang });
    }

    setDefaultLang(lang: string) {
    this.defaultLang = lang;
    }

    setTranslation(_: string, __: any, ___?: boolean) {}

    getBrowserLang() {
    return 'en';
    }

    setFallbackLang(_: string) {}
    }

    @NgModule({
    imports: [
    TranslateMockPipe,
    TranslateModule.forRoot({
    loader: {
    provide: TranslateLoader,
    useClass: LoaderMock,
    },
    }),
    ],
    declarations: [],
    providers: [
    { provide: TranslateService, useClass: TranslateMockService },
    {
    provide: TranslatePipe,
    useClass: TranslateMockPipe,
    },
    ],
    exports: [TranslateMockPipe, TranslateModule],
    })
    export class TranslateTestingModule {}

    Continue reading...

Compartilhe esta Página