spectator icon indicating copy to clipboard operation
spectator copied to clipboard

`createHostFactory` with `declareComponent: false` can't find tested component in the host with Vitest test runner

Open th0r opened this issue 5 months ago • 8 comments

Is this a regression?

No

Description

I have a module ButtonModule with a single non-standalone ButtonComponent exported from it.

As a unit tests runner I use Angular's experimental @angular/build:unit-test builder with the following configuration:

"test": {
  "builder": "@angular/build:unit-test",
  "options": {
    "runner": "vitest",
    "browsers": [
      "chromium"
    ],
    "buildTarget": "::development",
    "tsConfig": "tsconfig.spec.json",
    "codeCoverage": false
  }
}

In the ButtonComponent's spec there is the following code:

import {ButtonComponent} from './button.component';
import {
  createHostFactory,
  SpectatorHost
} from '@ngneat/spectator/vitest';
import {ButtonModule} from './button-module';

describe('ButtonComponent', () => {
  let spectator: SpectatorHost<ButtonComponent>;
  const createComponentHost = createHostFactory({
    component: ButtonComponent,
    imports: [ButtonModule],
    declareComponent: false
  });

  it('should create', () => {
    spectator = createComponentHost('<app-button>');

    expect(spectator.query('button')).toExist();
  });
});

Note declareComponent: false option. Running npm test leads to the following runtime error:

Error: Cannot find component/directive class _ButtonComponent {
  static \u0275fac = function ButtonComponent_Factory(__ngFactoryType__) {
    return new (__ngFactoryType__ || _ButtonComponent)();
  };
  static \u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _ButtonComponent, selectors: [["app-button"]], standalone: false, ngContentSelectors: _c0, decls: 2, vars: 0, template: function ButtonComponent_Template(rf, ctx) {
    if (rf & 1) {
      \u0275\u0275projectionDef();
      \u0275\u0275elementStart(0, "button");
      \u0275\u0275projection(1);
      \u0275\u0275elementEnd();
    }
  }, encapsulation: 2 });
} in host template 😔

Changing the test config to the default one based on Karma (see below) and changing the import in button.component.spec.ts from @ngneat/spectator/vitest to @ngneat/spectator fixes the problem and the test passes:

"test": {
  "builder": "@angular/build:karma",
  "options": {
    "polyfills": [
      "zone.js",
      "zone.js/testing"
    ],
    "tsConfig": "tsconfig.spec.json",
    "assets": [
      {
        "glob": "**/*",
        "input": "public"
      }
    ],
    "styles": [
      "src/styles.css"
    ]
  }
}

In the reproduction repo run npm i && npm test and you'll see the error.

Please provide a link to a minimal reproduction of the bug

https://github.com/th0r/ng-neat-vitest-host-component-bug

Please provide the exception or error you saw

Error: Cannot find component/directive class _ButtonComponent {
  static \u0275fac = function ButtonComponent_Factory(__ngFactoryType__) {
    return new (__ngFactoryType__ || _ButtonComponent)();
  };
  static \u0275cmp = /* @__PURE__ */ \u0275\u0275defineComponent({ type: _ButtonComponent, selectors: [["app-button"]], standalone: false, ngContentSelectors: _c0, decls: 2, vars: 0, template: function ButtonComponent_Template(rf, ctx) {
    if (rf & 1) {
      \u0275\u0275projectionDef();
      \u0275\u0275elementStart(0, "button");
      \u0275\u0275projection(1);
      \u0275\u0275elementEnd();
    }
  }, encapsulation: 2 });
} in host template 😔

Please provide the environment you discovered this bug in

Angular CLI: 20.2.2
Node: 22.16.0
Package Manager: npm 10.9.3
OS: darwin arm64


Angular: 20.2.4
... common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                      Version
------------------------------------
@angular-devkit/architect    0.2002.2
@angular-devkit/core         20.2.2
@angular-devkit/schematics   20.2.2
@angular/build               20.2.2
@angular/cli                 20.2.2
@schematics/angular          20.2.2
rxjs                         7.8.2
typescript                   5.9.2
zone.js                      0.15.1

Anything else?

No response

Do you want to create a pull request?

No

th0r avatar Sep 09 '25 13:09 th0r

@chimurai could you take a look please? It looks like <app-button> component is not registered for some reason as in the Vitest console I see the following message:

NG0304: 'app-button' is not a known element (used in the '_HostComponent' component template):
1. If 'app-button' is an Angular component, then verify that it is a part of an @NgModule where this component is declared.
2. If 'app-button' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.

th0r avatar Sep 17 '25 16:09 th0r

Isn't your case in the same purpose described in Testing Module in the docs? If so, why are you using createHostFactory instead of createComponentFactory?

KlausEverWalkingDev avatar Sep 21 '25 18:09 KlausEverWalkingDev

What do you mean by my case? I've just created a smallest example to show the bug in createHostFactory.

th0r avatar Sep 21 '25 19:09 th0r

I meant you're not using it as a host, and not even with the options presented at Testing With Host, although declareComponent do exists as one (because SpectatorHostOptions extends SpectatorOptions type).

KlausEverWalkingDev avatar Sep 22 '25 02:09 KlausEverWalkingDev

@KlausEverWalkingDev what options? hostProps? All those options are optional as I may want to just test that my component correctly projects the content I passed to it e.g.

spectator = createHost(`<app-button>Some content</app-button>`);
expect(spectator.hostElement).toHaveText('Some content');

But all of this doesn't matter as what I tried to show in the spec example is that the line spectator = createComponentHost('<app-button>') immediately throws an error as the code in the createComponentHost function can't locate the instance of the tested component. So even if it would be something like

createComponentHost('<app-button>Some content</app-button>')

it would immediately fail anyway.

th0r avatar Sep 22 '25 14:09 th0r

What I'm failing to understand is why are you passing declareComponent when in the it's nowhere to be found when you testing with host.

Image

If you wanted to test the module though:

Image

That option just makes sense when you test modules, where you see you have to use createComponentFactory and not createComponentHost or createHostFactory.

KlausEverWalkingDev avatar Sep 23 '25 00:09 KlausEverWalkingDev

Because it's not a standalone component. In my example the tested component (app-button) doesn't have any dependencies, but in the real world use case it may use tens of dependent components/directives/pipes which you would have to declare manually in createHostFactory. So there is a way to avoid that - you can just import the whole module, which exports the tested component, and declareComponent in this case says that there is no need to declare (register) the tested component again, as it's already declared (exported from the module).

th0r avatar Sep 23 '25 06:09 th0r

@NetanelBasal @chimurai any feedback guys?

th0r avatar Sep 29 '25 15:09 th0r