Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug (or Docs Bug?): Unable to test recursive standalone component due to NG0300 #50525

Closed
bbarry opened this issue May 30, 2023 · 7 comments
Closed
Assignees
Labels
area: core Issues related to the framework runtime bug state: has PR
Milestone

Comments

@bbarry
Copy link

bbarry commented May 30, 2023

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

Yes

Description

I have a relatively simple component that recursively nests on itself to build a node tree of div tags with various css classes and inner text based on a configuration object. When I convert this component to a standalone component, my tests fail with NG0300 but the new component works fine in the application. I'm assuming I am doing something wrong in configuring the testing module or that there is some bug in it but I'm unable to find documentation covering this case. The full component:

import { Input, Component, forwardRef } from '@angular/core';
import { NgFor, NgIf, NgClass } from '@angular/common';

export interface Node {
  cssClasses?: string;
  text?: string;
  nodes?: Node[];
}

@Component({
  selector: '[app-nester]',
  template: `<ng-container *ngFor="let node of nodes">
    <ng-container>
      <ng-container *ngIf="node.text as text">
        <div [ngClass]="node.cssClasses!">{{ text }}</div>
      </ng-container>
      <ng-container *ngIf="node.nodes as nodes">
        <div [ngClass]="node.cssClasses!" app-nester [nodes]="node.nodes"></div>
      </ng-container>
    </ng-container>
  </ng-container> `,
  // standalone: true,
  // imports: [NgFor, NgIf, NgClass, forwardRef(() => NesterComponent)],
})
export class NesterComponent {
  @Input() nodes: Node[] | null = null;
}

to convert to standalone I used the new migration and it added these commented out lines and imports.

My Tests:

import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { NesterComponent } from './nester.component';

describe('NesterComponent', () => {
  let component: NesterComponent;
  let fixture: ComponentFixture<NesterComponent>;

  // remove for standalone version
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({ declarations: [NesterComponent] }).compileComponents();
  }));

  // standalone version from migration:
  // beforeEach(waitForAsync(() => {
  //   TestBed.configureTestingModule({ imports: [NesterComponent] }).compileComponents();
  // }));

  beforeEach(() => {
    fixture = TestBed.createComponent(NesterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeDefined();
  });

  it('should render text', () => {
    component.nodes = [
      { cssClasses: 'test1', text: 'test1' },
      {
        cssClasses: 'test2',
        nodes: [{ cssClasses: 'not in result' }, { text: 'test4' }],
      },
      {
        cssClasses: 'test5',
        nodes: [],
      },
    ];
    fixture.detectChanges();
    expect(fixture.nativeElement).toMatchSnapshot();
  });
});

Again in this case all the migration did was change declarations to imports

The above functions and test passes... however if I switch it to the standalone version of both files then the test fails (as far as I can tell it still works though, this is just a test problem).

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

FAIL  src/app/shared/nester/nester.component.spec.ts (13.262 s)
  NesterComponent
    √ should create (322 ms)
    × should render text (826 ms)

  ● NesterComponent › should render text

    NG0300: Multiple components match node with tagname div: NesterComponent and NesterComponent. Find more at https://angular.io/errors/NG0300

      at throwMultipleComponentError (node_modules/@angular/core/fesm2022/core.mjs:10251:11)
      at findDirectiveDefMatches (node_modules/@angular/core/fesm2022/core.mjs:11580:29)
      at resolveDirectives (node_modules/@angular/core/fesm2022/core.mjs:11380:29)
      at elementStartFirstCreatePass (node_modules/@angular/core/fesm2022/core.mjs:15137:5)
      at ɵɵelementStart (node_modules/@angular/core/fesm2022/core.mjs:15173:9)
      at ɵɵelement (node_modules/@angular/core/fesm2022/core.mjs:15254:5)
      at NesterComponent_ng_container_0_ng_container_1_ng_container_2_Template (ng:/NesterComponent.js:25:5)
      at executeTemplate (node_modules/@angular/core/fesm2022/core.mjs:10890:17)
      at renderView (node_modules/@angular/core/fesm2022/core.mjs:12078:13)
      at TemplateRef2.createEmbeddedViewImpl (node_modules/@angular/core/fesm2022/core.mjs:22877:9)
      at ViewContainerRef2.createEmbeddedView (node_modules/@angular/core/fesm2022/core.mjs:23142:37)
      at _NgIf._updateView (node_modules/@angular/common/fesm2022/common.mjs:3316:45)
      at _NgIf.set ngIf [as ngIf] (node_modules/@angular/common/fesm2022/common.mjs:3289:14)
      at writeToDirectiveInput (node_modules/@angular/core/fesm2022/core.mjs:11766:33)
      at setInputsForProperty (node_modules/@angular/core/fesm2022/core.mjs:12000:9)
      at elementPropertyInternal (node_modules/@angular/core/fesm2022/core.mjs:11302:9)
      at ɵɵproperty (node_modules/@angular/core/fesm2022/core.mjs:15115:9)
      at NesterComponent_ng_container_0_ng_container_1_Template (ng:/NesterComponent.js:48:5)
      at ReactiveLViewConsumer.runInContext (node_modules/@angular/core/fesm2022/core.mjs:10345:13)
      at executeTemplate (node_modules/@angular/core/fesm2022/core.mjs:10885:22)
      at refreshView (node_modules/@angular/core/fesm2022/core.mjs:12395:13)
      at refreshEmbeddedViews (node_modules/@angular/core/fesm2022/core.mjs:12506:17)
      at refreshView (node_modules/@angular/core/fesm2022/core.mjs:12419:9)
      at refreshEmbeddedViews (node_modules/@angular/core/fesm2022/core.mjs:12506:17)
      at refreshView (node_modules/@angular/core/fesm2022/core.mjs:12419:9)
      at refreshComponent (node_modules/@angular/core/fesm2022/core.mjs:12542:13)
      at refreshChildComponents (node_modules/@angular/core/fesm2022/core.mjs:12589:9)
      at refreshView (node_modules/@angular/core/fesm2022/core.mjs:12445:13)
      at detectChangesInternal (node_modules/@angular/core/fesm2022/core.mjs:12337:9)
      at RootViewRef.detectChanges (node_modules/@angular/core/fesm2022/core.mjs:12867:9)
      at ComponentFixture._tick (node_modules/@angular/core/fesm2022/testing.mjs:126:32)
      at node_modules/@angular/core/fesm2022/testing.mjs:139:22
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (node_modules/zone.js/bundles/zone.umd.js:416:30)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/bundles/zone-testing.umd.js:300:43)
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (node_modules/zone.js/bundles/zone.umd.js:415:56)
      at Object.onInvoke (node_modules/@angular/core/fesm2022/core.mjs:26108:33)
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (node_modules/zone.js/bundles/zone.umd.js:415:56)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/bundles/zone.umd.js:173:47)
      at NgZone.run (node_modules/@angular/core/fesm2022/core.mjs:25962:28)
      at ComponentFixture.detectChanges (node_modules/@angular/core/fesm2022/testing.mjs:138:25)
      at src/app/shared/nester/nester.component.spec.ts:37:13
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (node_modules/zone.js/bundles/zone.umd.js:416:30)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/bundles/zone-testing.umd.js:300:43)
      at _ZoneDelegate.Object.<anonymous>._ZoneDelegate.invoke (node_modules/zone.js/bundles/zone.umd.js:415:56)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/bundles/zone.umd.js:173:47)
      at Object.wrappedFunc (node_modules/zone.js/bundles/zone-testing.umd.js:780:34)

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 16.0.2
Node: 18.16.0
Package Manager: npm 9.5.1
OS: win32 x64

Angular: 16.0.2
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, localize, platform-browser
... platform-browser-dynamic, router

Package                         Version
@angular-devkit/architect       0.1502.6
@angular-devkit/build-angular   16.0.2
@angular-devkit/core            15.2.6
@angular-devkit/schematics      16.0.2
@angular/cdk                    16.0.1
@angular/material               16.0.1
@schematics/angular             16.0.2
rxjs                            7.8.1
typescript                      5.0.4

Anything else?

It would be nice if https://angular.io/guide/testing-components-basics said something about standalone components. And/Or if https://angular.io/guide/standalone-components or https://angular.io/guide/standalone-migration covered testing at all.

@JeanMeche
Copy link
Member

The import forwardRef(() => NesterComponent seems superfluous to me. Removing it, seems to fix your issue. Why do you add it in the first place ?

@bbarry
Copy link
Author

bbarry commented Jun 1, 2023

It was added by ng generate @angular/core:standalone

It does seem to fix the issue. I guess we don't need a forwardRef for this specific type of recursion?

@JeanMeche
Copy link
Member

JeanMeche commented Jun 2, 2023

I'm pinging @crisbeto for this one, the migrations shouldn't have added it.

But there is also still an issue with the TestBed, it works at runtime it shouldn't fail with the TestBed.

Edit: I've opened #50554 to fix the migration. I'll investigate also the TestBed issue.

JeanMeche added a commit to JeanMeche/angular that referenced this issue Jun 2, 2023
This commit fixes the migrations for recursive components.

fixes angular#50525
@crisbeto
Copy link
Member

crisbeto commented Jun 2, 2023

While the forwardRef here isn't strictly necessary, I think this also shows an issue in the compiler. The forwardRef here should basically be a noop.

JeanMeche added a commit to JeanMeche/angular that referenced this issue Jun 2, 2023
This commit fixes the migrations for recursive components.

fixes angular#50525
@JeanMeche
Copy link
Member

I've reproduced the issue in an app, it happens when the component is compiled with the JIT compiler + has circular forwardRef + is made a container (like when injecting the ViewContainerRef).

@Component({
  selector: 'child',
  template: '<ng-container *ngIf="nested"><child [nested]="false"/></ng-container>',
  standalone: true,
  imports: [CommonModule, forwardRef(() => ChildComponent)],
})
export class ChildComponent {
  title = 'test';
  constructor(vcr: ViewContainerRef) { }

  @Input() nested: boolean = true;
}

@crisbeto
Copy link
Member

crisbeto commented Jun 2, 2023

It's actually even simpler: if a component sets itself in the imports it will throw the error even if it's not an infinite recursion. I have a decent idea where the issue is, I just had to step outside so I didn't test it out. This is a unit test to reproduce it:

    fit('should not throw if a standalone component imports itself', () => {
      @Component({
        selector: 'comp',
        template: '<comp *ngIf="recurse"/>',
        standalone: true,
        imports: [Comp, NgIf]
      })
      class Comp {
        @Input() recurse = false;
      }

      @Component({template: '<comp [recurse]="true"/>', standalone: true, imports: [Comp]})
      class App {
      }

      expect(() => {
        const fixture = TestBed.createComponent(App);
        fixture.detectChanges();
      }).not.toThrow();
    });

JeanMeche added a commit to JeanMeche/angular that referenced this issue Jun 2, 2023
Before this fix, a self import would define the component twice in the `directiveRegistry` which would then fire a `NG0300` when compiled with the JIT.

fixes angular#50525
JeanMeche added a commit to JeanMeche/angular that referenced this issue Jun 2, 2023
Before this fix, a self import would define the component twice in the `directiveRegistry` which would then fire a `NG0300` when compiled with the JIT.

fixes angular#50525
crisbeto added a commit to crisbeto/angular that referenced this issue Jun 2, 2023
Components are implied to be self-referencing, but if they explicitly set themselves in the `imports` array, they would throw an error because we weren't filtering them out.

Fixes angular#50525.
@crisbeto crisbeto self-assigned this Jun 2, 2023
@crisbeto crisbeto added area: core Issues related to the framework runtime state: has PR bug labels Jun 2, 2023
@ngbot ngbot bot modified the milestone: needsTriage Jun 2, 2023
JeanMeche added a commit to JeanMeche/angular that referenced this issue Jun 2, 2023
This commit fixes the migrations for recursive components.

fixes angular#50525
@alxhub alxhub closed this as completed in 79a706c Jun 6, 2023
alxhub pushed a commit that referenced this issue Jun 6, 2023
…50559)

Components are implied to be self-referencing, but if they explicitly set themselves in the `imports` array, they would throw an error because we weren't filtering them out.

Fixes #50525.

PR Close #50559
pkozlowski-opensource pushed a commit that referenced this issue Jun 14, 2023
This commit fixes the migrations for recursive components.

fixes #50525

PR Close #50554
pkozlowski-opensource pushed a commit that referenced this issue Jun 14, 2023
This commit fixes the migrations for recursive components.

fixes #50525

PR Close #50554
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Jul 7, 2023
thomasturrell pushed a commit to thomasturrell/angular that referenced this issue Aug 29, 2023
…ngular#50559)

Components are implied to be self-referencing, but if they explicitly set themselves in the `imports` array, they would throw an error because we weren't filtering them out.

Fixes angular#50525.

PR Close angular#50559
thomasturrell pushed a commit to thomasturrell/angular that referenced this issue Aug 29, 2023
…50554)

This commit fixes the migrations for recursive components.

fixes angular#50525

PR Close angular#50554
ChellappanRajan pushed a commit to ChellappanRajan/angular that referenced this issue Jan 23, 2024
…ngular#50559)

Components are implied to be self-referencing, but if they explicitly set themselves in the `imports` array, they would throw an error because we weren't filtering them out.

Fixes angular#50525.

PR Close angular#50559
ChellappanRajan pushed a commit to ChellappanRajan/angular that referenced this issue Jan 23, 2024
…50554)

This commit fixes the migrations for recursive components.

fixes angular#50525

PR Close angular#50554
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.