shallow-render: Not working with library input components that have implemented ControlValueAccessor

I’m getting this error

error properties: Object({ originalStack: 'Error: No value accessor for form control with unspecified name attribute

when rendering an external library component that has ControlValueAccessor implemented. The library components structure is this

export class InputComponent extends InputBase<string> {
// and base 
export abstract class InputBase<T> implements ControlValueAccessor {

Component is used like this

<my-input
  #hexInput
  (input)="onKey(hexInput.value)"
  [(ngModel)]="currentColor"
  [label]="'HEX code'"
  [helperText]="'Please add a valid HEX color'"
  [invalid]="invalidHex"
></my-input>

I can fix the issue by adding ngDefaultControl attribute but afaik this issue occurs only because the component is not recognized as having implemented ControlValueAccessor.

Can you please assist with this?

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 22 (12 by maintainers)

Most upvoted comments

Ok, so we managed to do a workaround in our app. We have around 1300 test files and manually adding ngDefaultControl wasn’t even an option.

Quick solution

import { Directive, NgModule } from '@angular/core';
import { DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Shallow } from 'shallow-render';

@Directive({
    // tslint:disable-next-line:directive-selector
    selector: '[formControl], [formControlName], [ngModel]',
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: Angular10ShallowCompatibilityDirective,
            multi: true,
        },
    ],
})
class Angular10ShallowCompatibilityDirective extends DefaultValueAccessor {}

@NgModule({
    declarations: [Angular10ShallowCompatibilityDirective],
    exports: [Angular10ShallowCompatibilityDirective],
})
class Angular10ShallowCompatibilityModdule {}

export function shallowSupportForAngular10(): void {
    Shallow.alwaysImport(Angular10ShallowCompatibilityModdule);
    Shallow.neverMock(Angular10ShallowCompatibilityDirective);
}

The shallowSupportForAngular10 should be added in the main test entry point. In our case it was setup-jest.ts

// setup-jest.ts
...
shallowSupportForAngular10();
...

Explanation

Let’s say we have the <sps-example-test-control> component that prints hello on screen and logs when it is created:

@Component({
    selector: 'sps-example-test-control',
    template: '<div>hello</div>',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleTestControlComponent {

    constructor() {
        console.log('Initiating example test control');
    }
}

Now let’s use our component in test component and attach [(ngModel)] to it:

@Component({
    selector: 'sps-example-test',
    template: `
        <sps-example-test-control name="age" [(ngModel)]="myValue"></sps-example-test-control>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleTestComponent implements AfterViewInit {
    myValue = 'xyz';

    @ViewChild(NgModel) ngModel: NgModel;

    constructor() {
        console.log('hello');
    }

    ngAfterViewInit(): void {
        console.log(this.ngModel);
    }
}

Angular 9

What we should be getting is an error, because our custom test control doesn’t implement ControlValueAccessor interface, which is required for ngModel to function properly, right?

When we run this component in our app as a normal component it behaves correctly, it throws an error:

Uncaught Error: No value accessor for form control with name: 'age'

However!!!, our test cases pass!!! What? Why? Shouldn’t the tests throw when view throws?

describe('Test Examples', () => {
    let shallow: Shallow<ExampleTestComponent>;

    beforeEach(
        async(() => {
            shallow = new Shallow(ExampleTestComponent, ExampleTestModule)
        })
    );

    it('should create', async () => {
        const { instance } = await shallow.render();
        expect(instance).toBeTruthy();
    });
});

It turns out when we print ngModel instance in our tests we get the result below. It seems that Shallow for some reason automatically created mock of our <sps-example-test-control> and automatically attached it to ngModel as valueAccessor. It should not work like this!

console.log src/app/views/sps-app/views/examples/example-test/components/example-test/example-test.component.ts:17
hello

console.log src/app/views/sps-app/views/examples/example-test/components/example-test/example-test.component.ts:21
NgModel {
  _parent: null,
  name: 'age',
  valueAccessor: MockOfExampleTestControlComponent { <---- It created a mock of our host component and treated it as valueAccessor, so ngModel didn't complain
    __simulateChange: [Function],
    __simulateTouch: [Function],
    writeValue: [Function],
    __hide: [Function],
    __render: [Function]
  },
  _rawValidators: [],

Angular 10

In Angular 10 it was fixed, that means the test throws an error, because Shallow didn’t automagically treated our mock as valueAccessor. So everything should be fine, right? 😃

It turns out that all our valueAccessor bindings are performed via provider inside @Component({providers: [{provide: NG_VALUE_ACCESSOR, ...}]}) decorator, BUT because Shallow mocks all of the controls, the provider is never instantiated and thus each test case that uses our custom control is broken 😦

Hell yeah! Glad this worked out. I had been meaning to tackle component-level providers for a while but it hadn’t given anyone any trouble until you mentioned it.

Thanks for reporting. I’ll get a proper 10.1.0 release out soon and update the docs.

It might be due to the base mocks not implementing ControlValueAccessor but in my testing, that wasn’t necessary.

I just added a commit and pushed 10.1.0-1, give it a try and see if that resolves the issue.

Yeah, I can probably just solve this by implementing ControlValueAccessor in the default component/directive mocks. I’ll try to get this done today and push a minor release.