#Angular Testing Jest mocking translate pipe

43 messages · Page 1 of 1 (latest)

civic crater
#
it('should render title', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.nativeElement;
    let language = 'en';
    let title = 'Welcome to ...';
    if (localStorage.getItem('language')) {
      language = localStorage.getItem('language')!;
    } else if (navigator.language) {
      language = navigator.language.slice(0, 2);
    } else {
      language = 'en';
    }
    if (language === 'en') {
      title = 'Welcome to ...';
    } else if (language === 'de') {
      title = 'Willkomen zu ...';
    }
    expect(compiled.querySelector('#title')?.textContent).toContain(title);
  });

class AppConfigServiceStub {
  data = {
    "title": "Willkomen zu ...",
    "language": "de",
    "server": {
      "host": "localhost:",
      "port": "8000"
    }
  };

  // load(defaults?: AppConfig): Promise<AppConfig> {
  //   let data: AppConfig | any = {};
  //   return new Promise<AppConfig>(resolve => {
  //     data = Object.assign({}, defaults || {}, this.data || {});
  //     resolve(this.data);
  //   }
  //   );
  // }
}

class TranslateServiceStub {
  data = {
    "title": "Willkomen zu ...",
    "Language": "Sprache",
    "Change language:": "Sprache ändern:"
  }
}
runic locust
#

Then don't use the stub? And setup HttpTestingController to mock the responses that AppConfigService expects?

#

But what I see of your AppConfigServiceStub makes me thing that your AppConfigService should be more Observable-y

civic crater
#

the appConfigService.ts

@Injectable({
  providedIn: 'root'
})
export class AppConfigService {

  data: AppConfig | any = {};

  constructor(private http: HttpClient) { }

  load(defaults?: AppConfig): Promise<AppConfig> {
    return new Promise<AppConfig>(resolve => {
      this.http.get('app.config.json').subscribe({
        next: response => {
          console.log('using server-side configuration');
          this.data = Object.assign({}, defaults || {}, response || {});
          resolve(this.data);
        },
        error: error => {
          console.error(error);
          console.log('using default configuration');
          this.data = Object.assign({}, defaults || {});
          resolve(this.data);
        }
      });
    });
  }


  use(lang: string): Promise<Record<string, unknown>> {
    return new Promise<Record<string, unknown>>((resolve) => {
      const langPath = `./assets/languages/${lang || 'en'}.json`;

      this.http.get<Record<string, unknown>>(langPath).subscribe({
        next: response => {
          this.data = Object.assign({}, response || {});
          console.log(this.data);
          resolve(this.data);
        },
        error: err => {
          console.error(err);
          this.data = {};
          resolve(this.data);
        }
      }
      );
    });
  }
}
runic locust
#

g!any @civic crater

restive tapirBOT
#

@civic crater Try to avoid using any as a type. The TS compiler effectively treats any as “please turn off type checking for this thing”. https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#any

Always prefer to supply the proper type. You will benefit from the confidence that static typing brings, along with an enhanced developer experience through tooling (e.g. intellisense, refactoring, etc.).

In cases where you don’t know what type you want to accept, or when you want to accept anything because you will be blindly passing it through without interacting with it, you can use the unknown type. unknown introduction:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type and some practical examples: https://mariusschulz.com/blog/the-unknown-type-in-typescript.

runic locust
#
const DEFAULT_CONFIG: AppConfig = {
  // ???
} as const;
  
@Injectable({ providedIn: 'root' })
export class AppConfigService {
  public readonly data$: Observable<AppConfig>;

  constructor(private readonly http: HttpClient) {
    this.data$ = this.http.get<AppConfig>('app.config.json').pipe(
      map((serverConfig: AppConfig): AppConfig => {
        return {
          ...DEFAULT_CONFIG,
          ...serverConfig,
        };
      }),
      // Replay the most recent (bufferSize) emission on each subscription
      // Keep the buffered emission(s) (refCount) even after everyone unsubscribes. Can cause memory leaks.
      shareReplay({ bufferSize: 1, refCount: false }),
    );
  }
}
civic crater
#

translateService.ts

@Injectable({
  providedIn: 'root'
})
export class TranslateService {

  data: Record<string, unknown> = {}

  constructor(private http: HttpClient) { }

  use(lang: string): Promise<Record<string, unknown>> {
    return new Promise<Record<string, unknown>>((resolve) => {
      const langPath = `./assets/languages/${lang || 'en'}.json`;

      this.http.get<Record<string, unknown>>(langPath).subscribe({
        next: response => {
          this.data = Object.assign({}, response || {});
          config.language = lang;
          console.log(config.language);
          resolve(this.data);
        },
        error: err => {
          console.error(err);
          this.data = {};
          resolve(this.data);
        }
      }
      );
    });
  }
}
#

and in app.module.ts
I am loading the appConfig

export function setupAppConfigServiceFactory(
  service: AppConfigService) {
  return () => service.load();
}

export function setupTranslateServiceFactory(
  service: TranslateService) {
  return () => {
    try{
      if(localStorage.getItem('language')){
        service.use(localStorage.getItem('language')!);
      }else if(navigator.language){
        service.use(navigator.language);
      }else{
        service.use('en');
      }
    } catch(err){
      console.error(err);
    }
  }
}


@NgModule({
  declarations: [
    AppComponent,
    TreeviewComponent,
    TranslatePipe
  ],
  imports: [
    CommonModule,
    BrowserAnimationsModule,
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    MaterialModule,
  ],
  providers: [
    TranslateService,
  {
    provide: APP_INITIALIZER,
    useFactory: setupAppConfigServiceFactory,
    deps: [
      AppConfigService
    ],
    multi: true
  },
  {
    provide: APP_INITIALIZER,
    useFactory: setupTranslateServiceFactory,
    deps: [
      TranslateService
    ],
    multi: true
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }
civic crater
runic locust
#

It merges the objects into a new object.
It is the modern version of this.data = Object.assign({}, defaults || {}, response || {});

civic crater
#

ah cool.

i changed the appConfigService now to this:

@Injectable({
  providedIn: 'root'
})
export class AppConfigService {

  // data: AppConfig | any = {};

  DEFAULT_CONFIG: AppConfig = {
    title: 'Welcome to Bosch Data Editor NT',
    language: 'en'
  } as const;
  public readonly data$: Observable<AppConfig>;
  public translation$!: Observable<Record<string, unknown>>;

  constructor(private http: HttpClient) {
    this.data$ = this.http.get<AppConfig>('app.config.json').pipe(
      map((serverConfig: AppConfig): AppConfig => {
        return {
          ...serverConfig,
          ...this.DEFAULT_CONFIG
        };
      }),
      // Replay the most recent (bufferSize) emission on each subscription
      // Keep the buffered emissions (refcount) even after everyone unsubscribes to avoid memory leaks.
      share(),);
  }

  
  use(lang: string): Observable<Record<string, unknown>> {
    let translation = {}
    const langPath = `./assets/languages/${lang || 'en'}.json`;
    return this.translation$ = this.http.get<Record<string, unknown>>(langPath).pipe(
      map((translated: Record<string, unknown>): Record<string, unknown> => {
        return {
          ...translated,
          ...{}
        };
      }),
      shareReplay({ bufferSize: 1, refCount: false }),);
    }
  }
#

but if I console.log the appCOnfig data I get the data$ and DEFAULT_CONFIG

runic locust
#

This: ${lang || 'en'} is worrisome. Why would it be falsey?

civic crater
#

and if I subscribe the observable and console log .data it is the content of the default config

#

the 'en' is just a fallback for default language

runic locust
#

data$ is an Observable, you have to subscribe to it to get values out of it. In Angular you would chain that together with other Observables using the .pipe and then subscribe using the AsyncPipe (| async) in your Component template

#

use(lang: string = 'en'): Observable<

civic crater
#

so instead of first returning defaultconfig I returned the serverconfig

runic locust
#

Oh that is what you meant. Good catch

civic crater
#
export function setupAppConfigServiceFactory(
  service: AppConfigService) {
  return () => {
    try {
      if (service.data$) {
        console.log('using server-side configuration');
        service.data$.subscribe(data => {
          return data;
        }
        );
      } else {
        console.log('using default configuration');
        return service.DEFAULT_CONFIG;
      }
    } catch (err) {
      console.error(err);
    }
  }
}
#

oops, Not all code paths return a value

#

I can't return in catch defaultConfig

#

maybe I don't need try and catch

#

I don't know if it makes sense if I return err in catch since the app should load the server config and if not existing then take the defaultconfig

runic locust
#

You are making your life difficult by not just accepting the Observable patterns primarily used in Angular.

#
@Component({
  template: `
<main *ngIf="vm$ | async; else loading">
</main>
<ng-template #loading>Loading...</ng-template>
`,
})
export class AppComponent {
  public readonly vm$: Observable<unknown>; // Don't actually need a ViewModel in this example, but if you did that would be the type.

  constructor(
    private readonly configService: AppConfigService,
    private readonly titleService: Title,
  ) {
    this.vm$ = this.configService.data$.pipe(
      tap((config: AppConfig): void => {
        this.titleService.setTitle(config.theTitleOrWhatever);
      }),
    );
  }
}
civic crater
#

I find the angular documentation concerning testing a bit hard to understand. Is there better tutorials or videos you can recommend?

runic locust
#
  let mockConfigSub$: Subject<AppConfig>;

  beforeEach(async () => {
    mockConfigSub$ = new Subject<AppConfig>();

    await TestBed.configureTestingModule({
      providers: [ { provide: AppConfigService, useValue: { data$: mockConfigSub$ } } ],
civic crater
#

thanks man

#

@runic locust so here we mock just the observable data$ from the appConfigService right?

#

I am using this line to access the id of an html div element

expect(compiled.querySelector('#title')?.textContent).toContain(title);

but I get

Expected 'title' to contain 'Willkommen zu ...'
#

is something wrong in the syntax to get the text value of the id of the div element?

chrome summit
civic crater
civic crater
#

Angular Testing Jest mocking translate pipe

#

I updated my test concerning the translation and get the following error:

FAIL  src/app/app.component.spec.ts
  ● AppComponent › should render title

    expect(received).toContain(expected) // indexOf

    Expected substring: "Willkommen zu ..."
    Received string:    " title "

      104 |     //   title = 'Willkommen zu ...';
      105 |     // }
    > 106 |     expect(compiled.querySelector('#title')?.textContent).toContain(TRANSLATIONS_DE.title);
          |                                                           ^
      107 |   });
#

this is the part in the test file:

describe('AppComponent', () => {
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;
  let mockAppConfigService: AppConfigService;
  let mockTranslateService: TranslateService;
  let mockAppConfigSub$: Subject<AppConfig>;
  const TRANSLATIONS_DE = require('../assets/languages/de.json');
  const TRANSLATIONS_EN = require('../assets/languages/en.json');

  beforeEach(async () => {

    mockAppConfigSub$ = new Subject<AppConfig>();

    await TestBed.configureTestingModule({
      imports: [
        RouterTestingModule,
        HttpClientTestingModule,
      ],
      providers: [
        { provide: mockAppConfigService, useValue: { data$: mockAppConfigSub$ } },
        // TranslateService,
        // AppConfigService
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      declarations: [
        AppComponent,
        TranslatePipe
      ],
    }).compileComponents();

    // Inject the http service and test controller for each test
    httpClient = TestBed.inject(HttpClient);
    httpTestingController = TestBed.inject(HttpTestingController);
    mockAppConfigService = TestBed.inject(AppConfigService);
    mockTranslateService = TestBed.inject(TranslateService);
    // spyOn(mockTranslateService, 'use').and.returnValue();
  });

...

it('should render title', () => {

    let language = 'en';
    let title = 'Welcome to ...';
    const fixture = TestBed.createComponent(AppComponent);
    const compiled = fixture.nativeElement;
    
    mockTranslateService.use('de');
    fixture.detectChanges();
   expect(compiled.querySelector('#title')?.textContent).toContain(TRANSLATIONS_DE.title);
  });