tannerntannern / ts-mixer

A small TypeScript library that provides tolerable Mixin functionality.
MIT License
379 stars 27 forks source link

Using multiple Mixin classes with Angular annotation @Component leads to failing test using jest framework #66

Open TRUSTMEIMJEDI opened 8 months ago

TRUSTMEIMJEDI commented 8 months ago

I have angular 17 app made using nx repo

Code in runtime works fine even if my app is powered by TS 5.2.2

For simplicity, I made below code:

import { Mixin } from 'ts-mixer';
import { FilterComponent } from './extensions/filter.component';
import { SortComponent } from './extensions/sort.component';
import { SettingsComponent } from './extensions/settings.component';
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'ng-mf1-table',
  templateUrl: 'table.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TableComponent extends Mixin(FilterComponent, SortComponent, SettingsComponent) {

  constructor() {
    super();
  }

  getTest(): string {
    return `${this.getFilter()}_${this.getSort()}_${this.getSettings()}`;
  }
}
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'ng-mf1-sort',
  template: '',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SortComponent {
  getSort(): string {
    return 'sort';
  }
}
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'ng-mf1-settings',
  template: '',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SettingsComponent {
  getSettings(): string {
    return 'settings';
  }
}
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'ng-mf1-filter',
  template: '',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FilterComponent {

  getFilter(): string {
    return 'filter';
  }
}

When having multiple Mixin classes with @Component annotation below test fails

import { TableComponent } from './table.component';

describe('TableComponent', () => {

  it('test1', () => {
    const table = new TableComponent();

    expect(table.getTest()).toEqual('filter_sort_settings');
  });
});

with the following error:

  ● Test suite failed to run

    TypeError: Cannot redefine property: __annotations__
        at Function.defineProperties (<anonymous>)

      11 |   changeDetection: ChangeDetectionStrategy.OnPush,
      12 | })
    > 13 | export class TableComponent extends Mixin(FilterComponent, SortComponent, SettingsComponent) {
         |                                          ^
      14 |
      15 |   constructor() {
      16 |     super();

      at copyProps (../../node_modules/ts-mixer/dist/cjs/util.js:12:12)
      at hardMixProtos (../../node_modules/ts-mixer/dist/cjs/util.js:69:39)
      at Mixin (../../node_modules/ts-mixer/dist/cjs/mixins.js:38:36)
      at Object.<anonymous> (src/app/test/table.component.ts:13:42)
      at Object.<anonymous> (src/app/test/table.component.spec.ts:1:1)

if TableComponent extends multiple classes and only one has @Component annotation everything works fine, problem appears when at least 2 classes have this annotation.

The weird thing is that when "zone.js": "~0.14.0" is changed to "zone.js": "~0.11.0" there are no problem but Angular 17 support only zone.js >= 14.

I'm not sure if this problem is related to Jest/TestBed or ts-mixer because everything works fine, except testing.

tannerntannern commented 8 months ago

I'm not that familiar with Angular, but it's not clear to me how UI components could be "mixed". Don't they have to be composed in order to specify where they sit in the DOM tree?

Also, SortComponent, SettingsComponent and FilterComponent don't have a template, so do they need to be @components at all? Perhaps that was just left out of your example for simplicity?

TRUSTMEIMJEDI commented 8 months ago

Base components don't have to be annotated with @Component but when using angular features like @Input, @Output or using the DI injection mechanism then those base classes must have annotation @Component or @Directive when a base class has no template is recommended to use @Directive annotation, but a problem is that even by annotation @Directive there is still the same issue.

I went with a debugger through the code and I could not find any difference in the details of the same objects when have zone.js in version ~0.11.0 and ~0.14.0. By version ~0.14.0 of zone.js the difference is that on a second call of method hardMixProtos to copy static fields has inside call for method copyProps which tries to define property __annotation__ that is already existing in dest object because first call of hardMixProtos already defined that property.

image

Add extra value __annotations__ to excluded properties in the second call of method hardMixProtos like that

Object.setPrototypeOf(
        MixedClass,
        settings.staticsStrategy === 'copy'
            ? hardMixProtos(constructors, null, ['prototype', '__annotations__'])
            : proxyMix(constructors, Function.prototype)

fixes the problem.

As I mentioned before, running the angular app is not an issue, but when trying to run the Jest test for that component with or without using TestBed is an issue. I can share with you an example code.

TRUSTMEIMJEDI commented 3 months ago

@tannerntannern Did you see my last replay? ;)