Sam
Sam

Reputation: 6265

How to mock DialogService.open(...).whenClosed(...) with Jasmine?

We have some TypeScript code using the Aurelia framework and Dialog plugin that we are trying to test with Jasmine, but can't work out how to do properly.

This is the source function:

openDialog(action: string) {
    this._dialogService.open({ viewModel: AddAccountWizard })
        .whenClosed(result => {
            if (!result.wasCancelled && result.output) {
                const step = this.steps.find((i) => i.action === action);
                if (step) {
                    step.isCompleted = true;
                }
            }
        });
}

We can create a DialogService spy, and verify the open method easily - but we can't work out how to make the spy invoke the whenClosed method with a mocked result parameter so that we can then assert that the step is completed.

This is the current Jasmine code:

it("opens a dialog when clicking on incomplete bank account", async done => {
    // arrange
    arrangeMemberVerificationStatus();
    await component.create(bootstrap);
    const vm = component.viewModel as GettingStartedCustomElement;
    dialogService.open.and.callFake(() => {
        return { whenClosed: () => Promise.resolve({})};
    });

    // act
    $(".link, .-arrow")[0].click();

    // assert
    expect(dialogService.open).toHaveBeenCalledWith({ viewModel: AddAccountWizard });
    expect(vm.steps[2].isCompleted).toBeTruthy(); // FAILS

    done();
});

Upvotes: 1

Views: 1984

Answers (1)

AJ Karnitis
AJ Karnitis

Reputation: 195

We've just recently updated our DialogService and ran into the same issue, so we've made this primitive mock that suited our purposes so far. It's fairly limited and doesn't do well for mocking multiple calls with different results, but should work for your above case:

export class DialogServiceMock {
    shouldCancelDialog = false;
    leaveDialogOpen = false;
    desiredOutput = {};
    open = () => {
        let result = { wasCancelled: this.shouldCancelDialog, output: this.desiredOutput };
        let closedPromise = this.leaveDialogOpen ? new Promise((r) => { }) : Promise.resolve(result);
        let resultPromise = Promise.resolve({ closeResult: closedPromise });
        resultPromise.whenClosed = (callback) => {
            return this.leaveDialogOpen ? new Promise((r) => { }) : Promise.resolve(typeof callback == "function" ? callback(result) : null);
        };
        return resultPromise;
    };
}

This mock can be configured to test various responses, when a user cancels the dialog, and scenarios where the dialog is still open.

We haven't done e2e testing yet, so I don't know of a good way to make sure you wait until the .click() call finishes so you don't have a race condition between your expect()s and the whenClosed() logic, but I think you should be able to use the mock in the test like so:

it("opens a dialog when clicking on incomplete bank account", async done => {
    // arrange
    arrangeMemberVerificationStatus();
    await component.create(bootstrap);
    const vm = component.viewModel as GettingStartedCustomElement;

    let mockDialogService = new MockDialogService();
    vm.dialogService = mockDialogService; //Or however you're injecting the mock into the constructor; I don't see the code where you're doing that right now.
    spyOn(mockDialogService, 'open').and.callThrough();

    // act
    $(".link, .-arrow")[0].click();
    browser.sleep(100)//I'm guessing there's a better way to verify that it's finished with e2e testing, but the point is to make sure it finishes before we assert.

    // assert
    expect(mockDialogService.open).toHaveBeenCalledWith({ viewModel: AddAccountWizard });
    expect(vm.steps[2].isCompleted).toBeTruthy(); // FAILS

    done();
});

Upvotes: 1

Related Questions