scottohara/loot

View on GitHub
src/payees/controllers/index.test.ts

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
import type {
    StateMock,
    UibModalMock,
    UibModalMockResolves,
} from "~/mocks/node-modules/angular/types";
import type { ControllerTestFactory } from "~/mocks/types";
import type MockDependenciesProvider from "~/mocks/loot/mockdependencies";
import type { OgModalAlert } from "~/og-components/og-modal-alert/types";
import type { OgTableActionHandlers } from "~/og-components/og-table-navigable/types";
import type OgTableNavigableService from "~/og-components/og-table-navigable/services/og-table-navigable";
import type { Payee } from "~/payees/types";
import type PayeeIndexController from "~/payees/controllers";
import type { PayeeModelMock } from "~/mocks/payees/types";
import type { SinonStub } from "sinon";
import angular from "angular";
import createPayee from "~/mocks/payees/factories";
import sinon from "sinon";

describe("PayeeIndexController", (): void => {
    let payeeIndexController: PayeeIndexController,
        controllerTest: ControllerTestFactory,
        $transitions: angular.ui.IStateParamsService,
        $timeout: angular.ITimeoutService,
        $uibModal: UibModalMock,
        $state: StateMock,
        payeeModel: PayeeModelMock,
        ogTableNavigableService: OgTableNavigableService,
        payees: Payee[],
        deregisterTransitionSuccessHook: SinonStub;

    // Load the modules
    beforeEach(
        angular.mock.module(
            "lootMocks",
            "lootPayees",
            (mockDependenciesProvider: MockDependenciesProvider): void =>
                mockDependenciesProvider.load([
                    "$uibModal",
                    "$state",
                    "payeeModel",
                    "payees",
                ]),
        ) as Mocha.HookFunction,
    );

    // Configure & compile the object under test
    beforeEach(
        angular.mock.inject(
            (
                _controllerTest_: ControllerTestFactory,
                _$transitions_: angular.ui.IStateParamsService,
                _$timeout_: angular.ITimeoutService,
                _$uibModal_: UibModalMock,
                _$state_: StateMock,
                _payeeModel_: PayeeModelMock,
                _ogTableNavigableService_: OgTableNavigableService,
                _payees_: Payee[],
            ): void => {
                controllerTest = _controllerTest_;
                $transitions = _$transitions_;
                $timeout = _$timeout_;
                $uibModal = _$uibModal_;
                $state = _$state_;
                payeeModel = _payeeModel_;
                ogTableNavigableService = _ogTableNavigableService_;
                payees = _payees_;
                deregisterTransitionSuccessHook = sinon.stub();
                sinon
                    .stub($transitions, "onSuccess")
                    .returns(deregisterTransitionSuccessHook);
                payeeIndexController = controllerTest(
                    "PayeeIndexController",
                ) as PayeeIndexController;
            },
        ) as Mocha.HookFunction,
    );

    it("should make the passed payees available to the view", (): Chai.Assertion =>
        expect(payeeIndexController.payees).to.deep.equal(payees));

    it("should focus the payee when a payee id is specified", (): void => {
        $state.params.id = "1";
        payeeIndexController = controllerTest("PayeeIndexController", {
            $state,
        }) as PayeeIndexController;
        payeeIndexController.tableActions.focusRow = sinon.stub();
        $timeout.flush();
        expect(
            (payeeIndexController.tableActions as OgTableActionHandlers).focusRow,
        ).to.have.been.calledWith(0);
    });

    it("should not focus the payee when a payee id is not specified", (): void =>
        $timeout.verifyNoPendingTasks());

    it("should register a success transition hook", (): Chai.Assertion =>
        expect($transitions.onSuccess).to.have.been.calledWith(
            { to: "root.payees.payee" },
            sinon.match.func,
        ));

    it("should deregister the success transition hook when the scope is destroyed", (): void => {
        (payeeIndexController as angular.IController).$scope.$emit("$destroy");
        expect(deregisterTransitionSuccessHook).to.have.been.called;
    });

    it("should ensure the payee is focussed when the payee id state param changes", (): void => {
        const toParams: { id: string } = { id: "1" };

        sinon.stub(
            payeeIndexController,
            "focusPayee" as keyof PayeeIndexController,
        );
        $transitions.onSuccess.firstCall.args[1]({
            params: sinon.stub().withArgs("to").returns(toParams),
        });
        expect(payeeIndexController["focusPayee"]).to.have.been.calledWith(
            Number(toParams.id),
        );
    });

    describe("editPayee", (): void => {
        let payee: Payee;

        beforeEach((): void => {
            sinon.stub(
                payeeIndexController,
                "focusPayee" as keyof PayeeIndexController,
            );
            payee = angular.copy(payeeIndexController.payees[1]);
        });

        it("should disable navigation on the table", (): void => {
            payeeIndexController.editPayee();
            expect(ogTableNavigableService.enabled).to.be.false;
        });

        describe("(edit existing)", (): void => {
            beforeEach((): void => payeeIndexController.editPayee(1));

            it("should open the edit payee modal with a payee", (): void => {
                expect($uibModal.open).to.have.been.called;
                expect(payeeModel.addRecent).to.have.been.calledWith(payee);
                expect(
                    ($uibModal.resolves as UibModalMockResolves).payee as Payee,
                ).to.deep.equal(payee);
            });

            it("should update the payee in the list of payees when the modal is closed", (): void => {
                payee.name = "edited payee";
                $uibModal.close(payee);
                expect(payeeIndexController.payees).to.include(payee);
            });
        });

        describe("(add new)", (): void => {
            beforeEach((): void => {
                payee = createPayee({
                    id: 999,
                    name: "new payee",
                });
                payeeIndexController.editPayee();
            });

            it("should open the edit payee modal without a payee", (): void => {
                expect($uibModal.open).to.have.been.called;
                expect(payeeModel.addRecent).to.not.have.been.called;
                expect(($uibModal.resolves as UibModalMockResolves).payee).to.be
                    .undefined;
            });

            it("should add the new payee to the list of payees when the modal is closed", (): void => {
                $uibModal.close(payee);
                expect(payeeIndexController.payees.pop() as Payee).to.deep.equal(payee);
            });

            it("should add the new payee to the recent list", (): void => {
                $uibModal.close(payee);
                expect(payeeModel.addRecent).to.have.been.calledWith(payee);
            });
        });

        it("should resort the payees list when the modal is closed", (): void => {
            const payeeWithHighestName: Payee = angular.copy(
                payeeIndexController.payees[2],
            );

            payeeIndexController.editPayee();
            $uibModal.close(payee);
            expect(payeeIndexController.payees.pop() as Payee).to.deep.equal(
                payeeWithHighestName,
            );
        });

        it("should focus the payee when the modal is closed", (): void => {
            payeeIndexController.editPayee();
            $uibModal.close(payee);
            expect(payeeIndexController["focusPayee"]).to.have.been.calledWith(
                payee.id,
            );
        });

        it("should not change the payees list when the modal is dismissed", (): void => {
            const originalPayees = angular.copy(payeeIndexController.payees);

            payeeIndexController.editPayee();
            $uibModal.dismiss();
            expect(payeeIndexController.payees).to.deep.equal(originalPayees);
        });

        it("should enable navigation on the table when the modal is closed", (): void => {
            payeeIndexController.editPayee();
            $uibModal.close(payee);
            expect(ogTableNavigableService.enabled).to.be.true;
        });

        it("should enable navigation on the table when the modal is dimissed", (): void => {
            payeeIndexController.editPayee();
            $uibModal.dismiss();
            expect(ogTableNavigableService.enabled).to.be.true;
        });
    });

    describe("deletePayee", (): void => {
        let payee: Payee;

        beforeEach(
            (): Payee => (payee = angular.copy(payeeIndexController.payees[1])),
        );

        it("should fetch the payee", (): void => {
            payeeIndexController.deletePayee(1);
            expect(payeeModel.find).to.have.been.calledWith(payee.id);
        });

        it("should disable navigation on the table", (): void => {
            payeeIndexController.deletePayee(1);
            expect(ogTableNavigableService.enabled).to.be.false;
        });

        it("should show an alert if the payee has transactions", (): void => {
            payeeIndexController.deletePayee(2);
            expect($uibModal.open).to.have.been.called;
            expect(
                (($uibModal.resolves as UibModalMockResolves).alert as OgModalAlert)
                    .header,
            ).to.equal("Payee has existing transactions");
        });

        it("should show the delete payee modal if the payee has no transactions", (): void => {
            payeeIndexController.deletePayee(1);
            expect($uibModal.open).to.have.been.called;
            expect(
                ($uibModal.resolves as UibModalMockResolves).payee as Payee,
            ).to.deep.equal(payee);
        });

        it("should remove the payee from the payees list when the modal is closed", (): void => {
            payeeIndexController.deletePayee(1);
            $uibModal.close(payee);
            expect(payeeIndexController.payees).to.not.include(payee);
        });

        it("should transition to the payees list when the modal is closed", (): void => {
            payeeIndexController.deletePayee(1);
            $uibModal.close(payee);
            expect($state.go).to.have.been.calledWith("root.payees");
        });

        it("should enable navigation on the table when the modal is closed", (): void => {
            payeeIndexController.deletePayee(1);
            $uibModal.close(payee);
            expect(ogTableNavigableService.enabled).to.be.true;
        });

        it("should enable navigation on the table when the modal is dimissed", (): void => {
            payeeIndexController.deletePayee(1);
            $uibModal.dismiss();
            expect(ogTableNavigableService.enabled).to.be.true;
        });
    });

    describe("toggleFavourite", (): void => {
        let payee: Payee;

        beforeEach((): Payee[] => ([payee] = payeeIndexController.payees));

        it("should favourite the payee", (): void => {
            payee.favourite = false;
            payeeIndexController.toggleFavourite(0);
            expect(payee.favourite).to.be.true;
        });

        it("should unfavourite the payee", (): void => {
            payee.favourite = true;
            payeeIndexController.toggleFavourite(0);
            expect(payee.favourite).to.be.false;
        });

        afterEach(
            (): Chai.Assertion =>
                expect(payeeModel.toggleFavourite).to.have.been.called,
        );
    });

    describe("tableActions.selectAction", (): void => {
        it("should transition to the payee transactions list", (): void => {
            payeeIndexController.tableActions.selectAction();
            expect($state.go).to.have.been.calledWith(".transactions");
        });
    });

    describe("tableActions.editAction", (): void => {
        it("should edit the payee", (): void => {
            sinon.stub(payeeIndexController, "editPayee");
            payeeIndexController.tableActions.editAction(1);
            expect(payeeIndexController["editPayee"]).to.have.been.calledWithExactly(
                1,
            );
        });
    });

    describe("tableActions.insertAction", (): void => {
        it("should insert a payee", (): void => {
            sinon.stub(payeeIndexController, "editPayee");
            payeeIndexController.tableActions.insertAction();
            expect(
                payeeIndexController["editPayee"],
            ).to.have.been.calledWithExactly();
        });
    });

    describe("tableActions.deleteAction", (): void => {
        it("should delete a payee", (): void => {
            sinon.stub(payeeIndexController, "deletePayee");
            payeeIndexController.tableActions.deleteAction(1);
            expect(
                payeeIndexController["deletePayee"],
            ).to.have.been.calledWithExactly(1);
        });
    });

    describe("tableActions.focusAction", (): void => {
        it("should focus a payee when no payee is currently focussed", (): void => {
            payeeIndexController.tableActions.focusAction(1);
            expect($state.go).to.have.been.calledWith(".payee", { id: 2 });
        });

        it("should focus a payee when another payee is currently focussed", (): void => {
            $state.currentState("**.payee");
            payeeIndexController.tableActions.focusAction(1);
            expect($state.go).to.have.been.calledWith("^.payee", { id: 2 });
        });
    });

    describe("focusPayee", (): void => {
        beforeEach(
            (): SinonStub =>
                (payeeIndexController.tableActions.focusRow = sinon.stub()),
        );

        it("should do nothing when the specific payee row could not be found", (): void => {
            expect(payeeIndexController["focusPayee"](999)).to.be.NaN;
            expect(
                (payeeIndexController.tableActions as OgTableActionHandlers).focusRow,
            ).to.not.have.been.called;
        });

        it("should focus the payee row for the specified payee", (): void => {
            const targetIndex: number = payeeIndexController["focusPayee"](1);

            $timeout.flush();
            expect(
                (payeeIndexController.tableActions as OgTableActionHandlers).focusRow,
            ).to.have.been.calledWith(targetIndex);
        });

        it("should return the index of the specified payee", (): void => {
            const targetIndex: number = payeeIndexController["focusPayee"](1);

            expect(targetIndex).to.equal(0);
        });
    });
});