NGO-DB/ndb-core

View on GitHub
src/app/core/common-components/entity-form/entity-form.service.spec.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { TestBed, waitForAsync } from "@angular/core/testing";

import { EntityFormService } from "./entity-form.service";
import {
  FormControl,
  UntypedFormControl,
  UntypedFormGroup,
} from "@angular/forms";
import { Entity } from "../../entity/model/entity";
import { School } from "../../../child-dev-project/schools/model/school";
import { ChildSchoolRelation } from "../../../child-dev-project/children/model/childSchoolRelation";
import { EntityAbility } from "../../permissions/ability/entity-ability";
import { InvalidFormFieldError } from "./invalid-form-field.error";
import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service";
import { Router } from "@angular/router";
import { NotFoundComponent } from "../../config/dynamic-routing/not-found/not-found.component";
import {
  EntitySchemaField,
  PLACEHOLDERS,
} from "../../entity/schema/entity-schema-field";
import { MockedTestingModule } from "../../../utils/mocked-testing.module";
import { ArrayDatatype } from "../../basic-datatypes/array/array.datatype";
import { EntityArrayDatatype } from "../../basic-datatypes/entity-array/entity-array.datatype";
import { Child } from "../../../child-dev-project/children/model/child";
import { DatabaseField } from "../../entity/database-field.decorator";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { FormFieldConfig } from "./FormConfig";
import { User } from "../../user/user";
import { TEST_USER } from "../../user/demo-user-generator.service";
import { CurrentUserSubject } from "../../session/current-user-subject";
import moment from "moment";

describe("EntityFormService", () => {
  let service: EntityFormService;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [MockedTestingModule.withState()],
    });
    service = TestBed.inject(EntityFormService);
  }));

  it("should be created", () => {
    expect(service).toBeTruthy();
  });

  it("should not save invalid entities", async () => {
    const entity = new Entity("initialId");
    const copyEntity = entity.copy();
    spyOn(entity, "copy").and.returnValue(copyEntity);
    spyOn(copyEntity, "assertValid").and.throwError(new Error());
    const formGroup = new UntypedFormGroup({
      _id: new FormControl("newId"),
    });

    await expectAsync(service.saveChanges(formGroup, entity)).toBeRejected();
    expect(entity.getId()).not.toBe(`${Entity.ENTITY_TYPE}:newId`);
  });

  it("should update entity if saving is successful", async () => {
    const entity = new Entity("initialId");
    const formGroup = new UntypedFormGroup({
      _id: new UntypedFormControl(`${Entity.ENTITY_TYPE}:newId`),
    });
    TestBed.inject(EntityAbility).update([
      { subject: "Entity", action: "create" },
    ]);

    await service.saveChanges(formGroup, entity);

    expect(entity.getId()).toBe(`${Entity.ENTITY_TYPE}:newId`);
  });

  it("should throw an error when trying to create a entity with missing permissions", async () => {
    TestBed.inject(EntityAbility).update([
      { subject: "all", action: "manage" },
      {
        subject: "School",
        action: "create",
        inverted: true,
        conditions: { name: "un-permitted school" },
      },
    ]);
    const school = new School();

    const formGroup = new UntypedFormGroup({
      name: new UntypedFormControl("normal school"),
    });
    await service.saveChanges(formGroup, school);
    expect(school.name).toBe("normal school");

    formGroup.patchValue({ name: "un-permitted school" });
    const result = service.saveChanges(formGroup, school);
    await expectAsync(result).toBeRejected();
    expect(school.name).toBe("normal school");
  });

  it("should create forms with the validators included", () => {
    const formFields = [{ id: "schoolId" }, { id: "result" }];
    const formGroup = service.createFormGroup(
      formFields,
      new ChildSchoolRelation(),
    );

    expect(formGroup.invalid).toBeTrue();
    formGroup.patchValue({ schoolId: "someSchool" });
    expect(formGroup.valid).toBeTrue();
    formGroup.patchValue({ result: 101 });
    expect(formGroup.invalid).toBeTrue();
    formGroup.patchValue({ result: 100 });
    expect(formGroup.valid).toBeTrue();
  });

  it("should use create permissions to disable fields when creating a new entity", () => {
    const formFields = [{ id: "name" }, { id: "dateOfBirth" }];
    TestBed.inject(EntityAbility).update([
      { subject: "Child", action: "read", fields: ["name", "dateOfBirth"] },
      { subject: "Child", action: "update", fields: ["name"] },
      { subject: "Child", action: "create", fields: ["dateOfBirth"] },
    ]);

    const formGroup = service.createFormGroup(formFields, new Child());

    expect(formGroup.get("name").disabled).toBeTrue();
    expect(formGroup.get("dateOfBirth").enabled).toBeTrue();
  });

  it("should always keep properties disabled if user does not have 'update' permissions for them", () => {
    const formFields = [{ id: "name" }, { id: "dateOfBirth" }];
    TestBed.inject(EntityAbility).update([
      { subject: "Child", action: "read", fields: ["name", "dateOfBirth"] },
      { subject: "Child", action: "update", fields: ["name"] },
    ]);

    const child = new Child();
    child._rev = "foo"; // "not new" state

    const formGroup = service.createFormGroup(formFields, child);

    expect(formGroup.get("name").enabled).toBeTrue();
    expect(formGroup.get("dateOfBirth").disabled).toBeTrue();

    formGroup.disable();

    expect(formGroup.get("name").disabled).toBeTrue();
    expect(formGroup.get("dateOfBirth").disabled).toBeTrue();

    formGroup.enable();

    expect(formGroup.get("name").enabled).toBeTrue();
    expect(formGroup.get("dateOfBirth").disabled).toBeTrue();
  });

  it("should create a error if form is invalid", () => {
    const formFields = [{ id: "schoolId" }, { id: "start" }];
    const formGroup = service.createFormGroup(
      formFields,
      new ChildSchoolRelation(),
    );

    return expectAsync(
      service.saveChanges(formGroup, new ChildSchoolRelation()),
    ).toBeRejectedWith(jasmine.any(InvalidFormFieldError));
  });

  it("should set pending changes once a form is edited and reset it once saved or canceled", async () => {
    const formFields = [{ id: "inactive" }];
    const formGroup = service.createFormGroup(formFields, new Entity());
    const unsavedChanges = TestBed.inject(UnsavedChangesService);

    formGroup.markAsDirty();
    formGroup.get("inactive").setValue(true);
    expect(unsavedChanges.pending).toBeTrue();

    TestBed.inject(EntityAbility).update([
      { action: "manage", subject: "all" },
    ]);
    await service.saveChanges(formGroup, new Entity());

    expect(unsavedChanges.pending).toBeFalse();

    formGroup.markAsDirty();
    formGroup.get("inactive").setValue(true);
    expect(unsavedChanges.pending).toBeTrue();

    service.resetForm(formGroup, new Entity());

    expect(unsavedChanges.pending).toBeFalse();
  });

  it("should reset form on cancel, including special fields with getter", async () => {
    class MockEntity extends Entity {
      @DatabaseField() simpleField = "original";

      @DatabaseField() get getterField(): string {
        return this._getterValue;
      }
      set getterField(value) {
        this._getterValue = value;
      }
      private _getterValue: string = "original value";

      @DatabaseField() emptyField;
    }

    const formFields = ["simpleField", "getterField", "emptyField"];
    const mockEntity = new MockEntity();
    const formGroup = service.createFormGroup(formFields, mockEntity);

    formGroup.get("simpleField").setValue("new");
    formGroup.get("getterField").setValue("new value");
    formGroup.get("emptyField").setValue("value");

    service.resetForm(formGroup, mockEntity);

    expect(formGroup.get("simpleField").value).toBe("original");
    expect(formGroup.get("getterField").value).toBe("original value");
    expect(formGroup.get("emptyField").value).toBeUndefined();
  });

  it("should reset state once navigation happens", async () => {
    const router = TestBed.inject(Router);
    router.resetConfig([{ path: "test", component: NotFoundComponent }]);
    const unsavedChanged = TestBed.inject(UnsavedChangesService);
    const formFields = [{ id: "inactive" }];
    const formGroup = service.createFormGroup(formFields, new Entity());
    formGroup.markAsDirty();
    formGroup.get("inactive").setValue(true);

    expect(unsavedChanged.pending).toBeTrue();

    await router.navigate(["test"]);

    expect(unsavedChanged.pending).toBeFalse();

    // Changes are not listened to anymore
    formGroup.markAsDirty();
    formGroup.get("inactive").setValue(true);

    expect(unsavedChanged.pending).toBeFalse();
  });

  it("should assign default values", () => {
    const schema: EntitySchemaField = { defaultValue: 1 };
    Entity.schema.set("test", schema);

    let form = service.createFormGroup([{ id: "test" }], new Entity());
    expect(form.get("test")).toHaveValue(1);

    schema.defaultValue = PLACEHOLDERS.NOW;
    form = service.createFormGroup([{ id: "test" }], new Entity());
    expect(
      moment(form.get("test").value).isSame(moment(), "minutes"),
    ).toBeTrue();

    schema.defaultValue = PLACEHOLDERS.CURRENT_USER;
    form = service.createFormGroup([{ id: "test" }], new Entity());
    expect(form.get("test")).toHaveValue(`${User.ENTITY_TYPE}:${TEST_USER}`);

    schema.dataType = ArrayDatatype.dataType;
    form = service.createFormGroup([{ id: "test" }], new Entity());
    expect(form.get("test")).toHaveValue([`${User.ENTITY_TYPE}:${TEST_USER}`]);

    schema.dataType = EntityArrayDatatype.dataType;
    form = service.createFormGroup([{ id: "test" }], new Entity());
    expect(form.get("test")).toHaveValue([`${User.ENTITY_TYPE}:${TEST_USER}`]);

    Entity.schema.delete("test");
  });

  it("should not fail if user entity does not exist and current user value is assigned", () => {
    TestBed.inject(CurrentUserSubject).next(undefined);

    // simple property
    Entity.schema.set("user", { defaultValue: PLACEHOLDERS.CURRENT_USER });
    let form = service.createFormGroup([{ id: "user" }], new Entity());
    expect(form.get("user")).toHaveValue(null);

    // array property
    Entity.schema.get("user").dataType = EntityArrayDatatype.dataType;
    form = service.createFormGroup([{ id: "user" }], new Entity());
    expect(form.get("user")).toHaveValue(null);

    Entity.schema.delete("user");
  });

  it("should not assign default values to existing entities", () => {
    Entity.schema.set("test", { defaultValue: 1 });

    const entity = new Entity();
    entity._rev = "1-existing_entity";
    const form = service.createFormGroup([{ id: "test" }], entity);
    expect(form.get("test")).toHaveValue(null);

    Entity.schema.delete("test");
  });

  it("should not overwrite existing values with default value", () => {
    Entity.schema.set("test", { defaultValue: 1 });

    const entity = new Entity();
    entity["test"] = 2;
    const form = service.createFormGroup([{ id: "test" }], entity);
    expect(form.get("test")).toHaveValue(2);

    Entity.schema.delete("test");
  });

  it("should add column definitions from property schema", () => {
    class Test extends Child {
      @DatabaseField({
        description: "Property description",
        additional: "someAdditional",
      })
      propertyField: string;
    }

    spyOn(TestBed.inject(EntitySchemaService), "getComponent").and.returnValue(
      "PredefinedComponent",
    );
    const entity = new Test();
    const field1: FormFieldConfig = {
      id: "fieldWithDefinition",
      editComponent: "EditComponent",
      viewComponent: "DisplayComponent",
      label: "Field with definition",
      description: "Custom tooltip",
      additional: "additional",
    };
    const field2: FormFieldConfig = { id: "propertyField", label: "Property" };

    const result1 = service.extendFormFieldConfig(
      field1,
      entity.getConstructor(),
    );
    const result2 = service.extendFormFieldConfig(
      field2,
      entity.getConstructor(),
    );

    expect(result1).toEqual({
      id: "fieldWithDefinition",
      editComponent: "EditComponent",
      viewComponent: "DisplayComponent",
      label: "Field with definition",
      forTable: false,
      description: "Custom tooltip",
      additional: "additional",
    } as FormFieldConfig);
    expect(result2).toEqual({
      id: "propertyField",
      dataType: "string",
      editComponent: "PredefinedComponent",
      viewComponent: "PredefinedComponent",
      label: "Property",
      forTable: false,
      description: "Property description",
      additional: "someAdditional",
    } as FormFieldConfig);
  });
});