NGO-DB/ndb-core

View on GitHub
src/app/features/matching-entities/matching-entities/matching-entities.component.spec.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import {
  ComponentFixture,
  fakeAsync,
  flush,
  TestBed,
  tick,
  waitForAsync,
} from "@angular/core/testing";

import { MatchingEntitiesComponent } from "./matching-entities.component";
import { MatchingEntitiesConfig } from "./matching-entities-config";
import { Entity } from "../../../core/entity/model/entity";
import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service";
import { Child } from "../../../child-dev-project/children/model/child";
import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation";
import { ActivatedRoute } from "@angular/router";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { ConfigService } from "../../../core/config/config.service";
import { BehaviorSubject, NEVER, Subject } from "rxjs";
import { FormFieldConfig } from "../../../core/common-components/entity-form/FormConfig";
import { Coordinates } from "../../location/coordinates";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { School } from "../../../child-dev-project/schools/model/school";
import { DynamicComponentConfig } from "../../../core/config/dynamic-components/dynamic-component-config.interface";
import { Note } from "../../../child-dev-project/notes/model/note";

describe("MatchingEntitiesComponent", () => {
  let component: MatchingEntitiesComponent;
  let fixture: ComponentFixture<MatchingEntitiesComponent>;

  let routeData: Subject<DynamicComponentConfig<MatchingEntitiesConfig>>;
  let mockConfigService: jasmine.SpyObj<ConfigService>;

  let testConfig: MatchingEntitiesConfig = {
    columns: [],
    onMatch: {
      newEntityMatchPropertyLeft: "schoolId",
      newEntityMatchPropertyRight: "childId",
      newEntityType: ChildSchoolRelation.ENTITY_TYPE,
    },
    matchActionLabel: "match test",
    rightSide: { entityType: "Child" },
    leftSide: { entityType: "School" },
  };

  beforeEach(waitForAsync(() => {
    routeData = new Subject();
    mockConfigService = jasmine.createSpyObj(["getConfig"], {
      configUpdates: NEVER,
    });

    TestBed.configureTestingModule({
      imports: [MatchingEntitiesComponent, MockedTestingModule.withState()],
      providers: [
        { provide: ActivatedRoute, useValue: { data: routeData } },
        { provide: FormDialogService, useValue: null },
        { provide: ConfigService, useValue: mockConfigService },
      ],
    }).compileComponents();
  }));

  beforeEach(waitForAsync(() => {
    fixture = TestBed.createComponent(MatchingEntitiesComponent);
    component = fixture.componentInstance;
  }));

  it("should create and map dynamic config to inputs", () => {
    Object.assign(component, testConfig);
    component.entity = new Entity();
    fixture.detectChanges();

    expectConfigToMatch(component, testConfig);
  });

  it("should create and map config from active route as alternative to dynamic config", () => {
    routeData.next({ config: testConfig });
    fixture.detectChanges();

    expectConfigToMatch(component, testConfig);
  });

  it("should use central default config and overwrite with dynamic config of the component", () => {
    testConfig.columns = [["defaultA", "defaultB"]];
    mockConfigService.getConfig.and.returnValue(testConfig);

    fixture = TestBed.createComponent(MatchingEntitiesComponent);
    component = fixture.componentInstance;
    component.entity = new Entity();
    fixture.detectChanges();

    expectConfigToMatch(component, testConfig);

    const currentConfig: MatchingEntitiesConfig = {
      columns: [["newA", "newB"]],
    };
    Object.assign(component, currentConfig);
    component.ngOnInit();

    const expectedCombinedConfig = Object.assign({}, testConfig, currentConfig);
    expectConfigToMatch(component, expectedCombinedConfig);
  });

  it("should assign config entity to the selected entity of the side not having a table with select options", fakeAsync(() => {
    const testEntity = new Entity("1");
    component.entity = testEntity;
    component.rightSide = { entityType: "Child" };
    component.onMatch = testConfig.onMatch;
    fixture.detectChanges();
    tick();

    expect(component.sideDetails[0].selected).toEqual([testEntity]);

    component.leftSide = { entityType: "Child" };
    component.rightSide = {};
    component.ngOnInit();
    tick();

    expect(component.sideDetails[1].selected).toEqual([testEntity]);
  }));

  it("should init details for template including available entities table and its columns", fakeAsync(() => {
    const testEntity = new Entity();
    component.entity = testEntity;
    component.rightSide = { entityType: "Child" };
    component.onMatch = testConfig.onMatch;
    component.columns = [
      ["_id", "name"],
      ["_rev", "phone"],
    ];
    const allChildren: Child[] = [Child.create("1"), Child.create("2")];
    const loadTypeSpy = spyOn(TestBed.inject(EntityMapperService), "loadType");
    loadTypeSpy.and.resolveTo(allChildren);

    fixture.detectChanges();
    tick();

    expect(component.sideDetails.length).toBe(2);

    expect(component.sideDetails[0].selected).toEqual([testEntity]);
    expect(component.sideDetails[0].entityType).toEqual(
      testEntity.getConstructor(),
    );
    expect(component.sideDetails[0].availableEntities).toBeUndefined();
    expect(component.sideDetails[0].columns).toEqual(["_id", "_rev"]);

    expect(component.sideDetails[1].selected).toBeUndefined();
    expect(component.sideDetails[1].entityType).toEqual(Child);
    expect(loadTypeSpy).toHaveBeenCalledWith(Child);
    expect(component.sideDetails[1].availableEntities).toEqual(allChildren);
    expect(component.sideDetails[1].columns).toEqual(["name", "phone"]);
  }));

  it("should select only one entity at a time in single select mode", fakeAsync(() => {
    const matchedEntity = Child.create("matched child");
    const otherMatchedEntity = Child.create("second matched child");

    Object.assign(component, testConfig);
    component.onMatch = {
      newEntityType: ChildSchoolRelation.ENTITY_TYPE,
      newEntityMatchPropertyRight: "childId",
      newEntityMatchPropertyLeft: "schoolId",
    };
    fixture.detectChanges();
    tick();
    const testedSide = component.sideDetails[1];

    testedSide.selectMatch(matchedEntity);
    expect(testedSide.selected).toEqual([matchedEntity]);

    testedSide.selectMatch(otherMatchedEntity);
    expect(testedSide.selected).toEqual([otherMatchedEntity]);
  }));

  it("should select multiple entities in multiSelect mode", fakeAsync(() => {
    const matchedEntity = Child.create("matched child");
    const otherMatchedEntity = Child.create("second matched child");

    Object.assign(component, testConfig);
    component.onMatch = {
      newEntityType: Note.ENTITY_TYPE,
      newEntityMatchPropertyRight: "children",
      newEntityMatchPropertyLeft: "schools",
    };
    fixture.detectChanges();
    tick();
    const testedSide = component.sideDetails[1];

    testedSide.selectMatch(matchedEntity);
    testedSide.selectMatch(otherMatchedEntity);
    expect(testedSide.selected).toEqual([matchedEntity, otherMatchedEntity]);

    testedSide.selectMatch(matchedEntity); // deselect by second click
    expect(testedSide.selected).toEqual([otherMatchedEntity]);
  }));

  it("should create a new entity onMatch, with single entity property", fakeAsync(() => {
    const testEntity = new Entity();
    const matchedEntity = Child.create("matched child");
    component.entity = testEntity;
    Object.assign(component, testConfig);
    component.onMatch = {
      newEntityType: ChildSchoolRelation.ENTITY_TYPE,
      newEntityMatchPropertyRight: "childId",
      newEntityMatchPropertyLeft: "schoolId",
    };
    component.columns = [["_id", "name"]];
    const saveSpy = spyOn(TestBed.inject(EntityMapperService), "save");

    fixture.detectChanges();
    tick();
    component.sideDetails[0].selected = [testEntity];
    component.sideDetails[1].selected = [matchedEntity];

    component.createMatch();
    tick();

    expect(saveSpy).toHaveBeenCalledWith(
      jasmine.objectContaining({
        schoolId: testEntity.getId(),
        childId: matchedEntity.getId(),
        name:
          "ChildSchoolRelation " + testEntity.toString() + " - matched child",
      } as Partial<ChildSchoolRelation>),
    );
    expect(saveSpy).toHaveBeenCalledWith(jasmine.any(ChildSchoolRelation));
  }));

  it("should create a new entity onMatch, with multiSelect entity-array property", fakeAsync(() => {
    const testEntity = new Entity();
    const child1 = Child.create("matched child 1");
    const child2 = Child.create("matched child 2");

    Object.assign(component, testConfig);
    component.onMatch = {
      newEntityType: Note.ENTITY_TYPE,
      newEntityMatchPropertyRight: "children",
      newEntityMatchPropertyLeft: "schools",
    };
    component.entity = testEntity;
    component.columns = [["_id", "name"]];
    const saveSpy = spyOn(TestBed.inject(EntityMapperService), "save");
    fixture.detectChanges();
    tick();

    component.sideDetails[0].selected = [testEntity];
    component.sideDetails[1].selected = [child1, child2];

    component.createMatch();
    tick();

    expect(saveSpy).toHaveBeenCalledWith(
      jasmine.objectContaining({
        schools: [testEntity.getId()],
        children: [child1.getId(), child2.getId()],
        name:
          "Note " +
          testEntity.toString() +
          " - matched child 1, matched child 2",
      } as Partial<ChildSchoolRelation>),
    );
    expect(saveSpy).toHaveBeenCalledWith(jasmine.any(Note));
  }));

  it("should create distance column and publish updates", fakeAsync(() => {
    Child.schema.set("address", { dataType: "location" });
    component.entity = new Child();
    component.columns = [[undefined, "distance"]];
    component.leftSide = { entityType: Child };
    component.onMatch = testConfig.onMatch;

    fixture.detectChanges();
    tick();

    const distanceColumn = component.columns[0][1] as FormFieldConfig;
    expect(distanceColumn).toEqual({
      id: "distance",
      label: "Distance",
      viewComponent: "DisplayDistance",
      additional: {
        coordinatesProperties: ["address"],
        compareCoordinates: jasmine.any(BehaviorSubject),
      },
    });

    let newCoordinates: Coordinates[];
    distanceColumn.additional.compareCoordinates.subscribe(
      (res) => (newCoordinates = res),
    );

    const compare = new Child();
    compare["address"] = { lat: 52, lon: 13 };

    component.sideDetails[0].selectMatch(compare);

    expect(newCoordinates).toEqual([compare["address"]]);

    Child.schema.delete("address");
  }));

  it("should select an entity if it has been selected in the map", fakeAsync(() => {
    component.entity = new Entity();
    component.rightSide = { entityType: "Child" };
    component.onMatch = testConfig.onMatch;
    fixture.detectChanges();
    tick();

    const child = new Child();
    component.entityInMapClicked(child);

    expect(component.sideDetails[1].selected).toEqual([child]);
  }));

  it("should not change the provided config object directly", fakeAsync(() => {
    Object.assign(component, testConfig);
    fixture.detectChanges();
    tick();
    const selectedChild = new Child();
    component.sideDetails[1].selectMatch(selectedChild);
    expect(component.sideDetails[1].selected).toEqual([selectedChild]);

    const newFixture = TestBed.createComponent(MatchingEntitiesComponent);
    const newComponent = newFixture.componentInstance;
    newComponent.entity = new Entity();
    Object.assign(newComponent, testConfig);
    newFixture.detectChanges();
    tick();

    expect(newComponent.sideDetails[1].selected).not.toEqual([selectedChild]);
  }));

  it("should update the distance calculation when the selected map properties change", fakeAsync(() => {
    Object.assign(component, testConfig);
    Child.schema.set("address", { dataType: "location" });
    Child.schema.set("otherAddress", { dataType: "location" });
    School.schema.set("address", { dataType: "location" });
    const leftEntity = new Child();
    leftEntity["address"] = { lat: 52, lon: 14 };
    leftEntity["otherAddress"] = { lat: 53, lon: 14 };
    const rightEntity1 = new School();
    rightEntity1["address"] = { lat: 52, lon: 13 };
    const rightEntity2 = new School();
    rightEntity2["address"] = { lat: 53, lon: 13 };
    spyOn(TestBed.inject(EntityMapperService), "loadType").and.resolveTo([
      rightEntity1,
      rightEntity2,
    ]);
    component.entity = leftEntity;
    component.columns = [];
    component.leftSide = {
      columns: ["distance"],
    };
    component.rightSide = {
      columns: ["distance"],
      entityType: "School",
    };
    fixture.detectChanges();
    tick();
    const leftSide = component.sideDetails[0];
    const rightSide = component.sideDetails[1];
    let lastLeftValue: Coordinates[];
    let lastRightValue: Coordinates[];
    leftSide.distanceColumn.compareCoordinates.subscribe(
      (res) => (lastLeftValue = res),
    );
    rightSide.distanceColumn.compareCoordinates.subscribe(
      (res) => (lastRightValue = res),
    );

    expect(lastLeftValue).toEqual([]);
    expect(lastRightValue).toEqual([
      leftEntity["address"],
      leftEntity["otherAddress"],
    ]);

    // values should be emitted again
    lastLeftValue = undefined;
    lastRightValue = undefined;
    // select only one property
    component.displayedProperties["Child"] = ["address"];
    component.updateMarkersAndDistances();

    expect(lastLeftValue).toEqual([]);
    expect(lastRightValue).toEqual([leftEntity["address"]]);

    // select an entity for right
    rightSide.selectMatch(rightEntity1);

    expect(lastLeftValue).toEqual([rightEntity1["address"]]);
    expect(lastRightValue).toEqual([leftEntity["address"]]);

    lastLeftValue = undefined;
    lastRightValue = undefined;
    //select both properties
    component.displayedProperties["Child"] = ["address", "otherAddress"];
    component.updateMarkersAndDistances();

    expect(lastLeftValue).toEqual([rightEntity1["address"]]);
    expect(lastRightValue).toEqual([
      leftEntity["address"],
      leftEntity["otherAddress"],
    ]);

    Child.schema.delete("otherAddress");
    Child.schema.delete("address");
    School.schema.delete("address");
    flush();
  }));

  it("should only display filtered entities in the map", fakeAsync(() => {
    const c1 = new Child();
    c1.status = "active";
    const c2 = new Child();
    c2.status = "inactive";
    c2.dropoutDate = new Date();
    const c3 = new Child();
    c3.status = "inactive";
    const other = new School();
    TestBed.inject(EntityMapperService).saveAll([c1, c2, c3, other]);
    tick();
    component.leftSide = {
      entityType: "Child",
      prefilter: { dropoutDate: { $exists: false } } as any,
      columns: ["status"],
    };
    component.rightSide = {
      entityType: "School",
      columns: ["_id"],
    };
    component.onMatch = testConfig.onMatch;
    fixture.detectChanges();
    tick();

    expect(component.filteredMapEntities.map((entity) => entity)).toEqual([
      c1,
      c3,
      other,
    ]);

    component.applySelectedFilters(component.sideDetails[0], {
      status: "active",
    } as any);

    expect(component.filteredMapEntities.map((entity) => entity)).toEqual([
      c1,
      other,
    ]);
  }));

  it("should display map if location properties are available", fakeAsync(() => {
    // Clean-up child schema before running test
    Child.schema.forEach((schema, name) => {
      if (schema.dataType === "location") {
        Child.schema.delete(name);
      }
    });
    component.mapVisible = false;
    component.entity = new Child();
    component.leftSide = { entityType: Child };
    component.onMatch = testConfig.onMatch;

    fixture.detectChanges();
    tick();

    expect(component.mapVisible).toBeFalse();

    Child.schema.set("address", { dataType: "location" });

    component.ngOnInit();
    tick();

    expect(component.mapVisible).toBeTrue();

    Child.schema.delete("address");
  }));

  it("should not alter the config object", fakeAsync(() => {
    const config: MatchingEntitiesConfig = {
      columns: [
        ["name", "name"],
        ["projectNumber", "distance"],
      ],
      rightSide: { entityType: "School", columns: ["name", "distance"] },
      leftSide: { entityType: "Child", columns: ["name", "distance"] },
      onMatch: testConfig.onMatch,
    };
    Child.schema.set("address", { dataType: "location" });
    School.schema.set("address", { dataType: "location" });

    const configCopy = JSON.parse(JSON.stringify(config));
    routeData.next({ config: configCopy });

    fixture.detectChanges();
    tick();

    expect(configCopy).toEqual(config);

    Child.schema.delete("address");
    School.schema.delete("address");
  }));

  it("should infer multiSelect mode from onMatch's entity schema", fakeAsync(() => {
    Object.assign(component, testConfig);
    component.onMatch = {
      newEntityType: ChildSchoolRelation.ENTITY_TYPE,
      newEntityMatchPropertyLeft: "childId",
      newEntityMatchPropertyRight: "schoolId",
    };
    component.ngOnInit();
    tick();

    expect(component.sideDetails[0].multiSelect).toBeFalse();
    expect(component.sideDetails[1].multiSelect).toBeFalse();

    component.onMatch = {
      newEntityType: Note.ENTITY_TYPE,
      newEntityMatchPropertyLeft: "children",
      newEntityMatchPropertyRight: "schools",
    };
    component.ngOnInit();
    tick();

    expect(component.sideDetails[0].multiSelect).toBeTrue();
    expect(component.sideDetails[1].multiSelect).toBeTrue();
  }));
});

function expectConfigToMatch(
  component: MatchingEntitiesComponent,
  configToLoad: MatchingEntitiesConfig,
) {
  expect(component.columns).toEqual(configToLoad.columns);
  expect(component.onMatch).toEqual(configToLoad.onMatch);
  expect(component.matchActionLabel).toEqual(configToLoad.matchActionLabel);
  expect(component.rightSide.entityType).toEqual(
    configToLoad.rightSide.entityType,
  );
  expect(component.leftSide.entityType).toEqual(
    configToLoad.leftSide.entityType,
  );
}