How to test ngSubmit in Angular
ngSubmit is an output of Angular's NgForm and FormGroupDirective.
When a form is submitted, Angular emits this output to call the handler bound in the template.
To test this behavior, keep FormsModule or ReactiveFormsModule and replace the component's handler with a spy.
This follows the same approach as form controls:
Angular's form directives stay real while application code can be mocked.
Related tools
Template-driven forms
Let's assume that TargetComponent uses this template:
<form (ngSubmit)="save(value, $event)">
<input
name="name"
[(ngModel)]="value"
[ngModelOptions]="{ updateOn: 'submit' }"
/>
<button type="submit" [disabled]="disabled">Save</button>
</form>
The component starts with value = 'initial' and disabled = false.
Its save(value, event) method handles the submitted value.
The component belongs to TargetModule, which imports FormsModule.
In the test, we replace save with a spy and submit the form with ngMocks.trigger.
The updateOn: 'submit' option lets us verify that Angular updates the value before calling save.
Import NgForm from @angular/forms to check the submitted state.
After MockRender, call await fixture.whenStable() to let ngModel register its control and apply the initial value.
describe('TestNgSubmit:template-driven', () => {
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule).keep(FormsModule),
);
it('calls save with the submitted value and event', async () => {
// Rendering the component and waiting for ngModel.
const fixture = MockRender(TargetComponent);
await fixture.whenStable();
const component = fixture.point.componentInstance;
const form = ngMocks.findInstance(NgForm);
// Replacing the application handler with a spy.
const save = jasmine.createSpy('save');
// in case of jest
// const save = jest.fn();
component.save = save;
expect(form.submitted).toBe(false);
expect(form.value).toEqual({ name: 'initial' });
// The value stays pending until the form is submitted.
ngMocks.change('input', 'updated');
expect(component.value).toBe('initial');
expect(form.value).toEqual({ name: 'initial' });
expect(save).not.toHaveBeenCalled();
// Angular handles submit and emits ngSubmit to call save.
const event = ngMocks.event('submit');
ngMocks.trigger('form', event);
expect(save).toHaveBeenCalledTimes(1);
expect(save).toHaveBeenCalledWith('updated', event);
expect(component.value).toBe('updated');
expect(form.value).toEqual({ name: 'updated' });
expect(form.submitted).toBe(true);
expect(event.defaultPrevented).toBe(true);
});
});
Reactive forms
For reactive forms, keep ReactiveFormsModule in the same way.
The component binds a FormGroup to the form:
<form
[formGroup]="form"
(ngSubmit)="save(form.controls.name.value, $event)"
>
<input formControlName="name" />
<button type="submit" [disabled]="disabled">Save</button>
</form>
Its form property uses FormControl and FormGroup from @angular/forms:
public readonly form = new FormGroup({
name: new FormControl('initial', {
updateOn: 'submit',
}),
});
The component's TargetModule imports ReactiveFormsModule.
Its save method and disabled property work as in the template-driven example.
Import FormGroupDirective from @angular/forms to check the submitted state.
The reactive form is registered synchronously in this example.
describe('TestNgSubmit:reactive', () => {
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule)
.keep(ReactiveFormsModule),
);
it('calls save with the submitted value and event', () => {
// Rendering the component and finding its form directive.
const component =
MockRender(TargetComponent).point.componentInstance;
const form = ngMocks.findInstance(FormGroupDirective);
// Replacing the application handler with a spy.
const save = jasmine.createSpy('save');
// in case of jest
// const save = jest.fn();
component.save = save;
expect(form.submitted).toBe(false);
expect(component.form.value).toEqual({ name: 'initial' });
// The value stays pending until the form is submitted.
ngMocks.change('input', 'updated');
expect(component.form.value).toEqual({ name: 'initial' });
expect(save).not.toHaveBeenCalled();
// Angular handles submit and emits ngSubmit to call save.
const event = ngMocks.event('submit');
ngMocks.trigger('form', event);
expect(save).toHaveBeenCalledTimes(1);
expect(save).toHaveBeenCalledWith('updated', event);
expect(component.form.value).toEqual({ name: 'updated' });
expect(form.submitted).toBe(true);
expect(event.defaultPrevented).toBe(true);
});
});
Submit buttons
To submit through the button, call its native click() method:
ngMocks.find('button').nativeElement.click();
expect(save).toHaveBeenCalledTimes(1);
The linked examples also assert that a native button click passes the updated value and submit event to save,
and that a disabled button does not call save.
Keep Angular's form directives real to test the connection from a native submit event to ngSubmit.
Emitting ngSubmit directly would skip Angular's submission handling, including pending form values.