diprokon / ng-table-virtual-scroll

Virtual Scroll for Angular Material Table
https://diprokon.github.io/ng-table-virtual-scroll
MIT License
133 stars 41 forks source link

I can't test the rendered rows in the table. #103

Open paologiarda opened 2 years ago

paologiarda commented 2 years ago

I try to run my unit tests with jest to check rendered rows in my table but no row are rendered. this is test in my spec.ts file: describe('ProjectsTableComponent', () => {

  let component: ProjectsTableComponent;

  let fixture: ComponentFixture;

  const testProjects: Project[] | null = [array containing two project];

  beforeEach(

    waitForAsync(() => {

      TestBed.configureTestingModule({

        imports: [

          CommonModule,

          BrowserModule,

          BrowserAnimationsModule,

          MaterialModule,

        ],

        declarations: [ProjectsTableComponent, BusinessUnitsBarComponent],

      }).compileComponents();

    })

  );

  beforeEach(() => {

    fixture = TestBed.createComponent(ProjectsTableComponent);

    component = fixture.componentInstance;

  });

  it('should create', () => {

    expect(component).toBeTruthy();

  });

  it('projects input should populate table', fakeAsync(() => {

    finishInit(fixture);

    component.projects = testProjects;

    fixture.detectChanges();

    flush();

    const compiled = fixture.debugElement.nativeElement;

    expect(compiled.querySelectorAll('.mat-row').length).toBeGreaterThanOrEqual(

      2

    );

  }));

});

/* Finish initializing the virtual scroll component at the beginning of a test. /

function finishInit(fixture: ComponentFixture) {

  // On the first cycle we render and measure the viewport.

  fixture.detectChanges();

  flush();

  // On the second cycle we render the items.

  fixture.detectChanges();

  flush();

  // Flush the initial fake scroll event.

  animationFrameScheduler.flush();

  flush();

  fixture.detectChanges();

}

my component.html

{{ getColumnLabel('projectNumber') }}
{{ element.projectNumber }}
{{ noDataMessage }}

my component.ts

export class ProjectsTableComponent implements OnInit { columns!: TableColumn[]; displayedColumns!: string[]; dataSource = new TableVirtualScrollDataSource(); private sort!: MatSort; noDataMessage = MatTableConstants.noDataMessage; @ViewChild(MatSort, { static: false }) set matSort(ms: MatSort) { this.sort = ms; this.setDataSourceAttributes(); } @Input() set projects(val: Project[] | null) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.dataSource.data = val as any; } @Output() selectedProject = new EventEmitter(); constructor() {}

ngOnInit(): void { this.columns = [ { fieldName: 'projectNumber', label: 'Project Number', }, ]; this.displayedColumns = this.columns.map((x) => x.fieldName); }

getColumnLabel(fieldName: string): string | null { return this.columns.find((x) => x.fieldName === fieldName)?.label || null; }

private setDataSourceAttributes() { this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = MatTableConstants.pathDataAccessor; } }

diprokon commented 1 year ago

Hi! Do you still have this problem? Maybe, you need to set height for viewport container - it's a common issue

Zoliqa commented 1 year ago

Hi, I'm actually having the same problem. I've tried to set the height of the viewport container and also some suggestions found here https://stackoverflow.com/questions/53726484/angular-material-virtual-scroll-not-rendering-items-in-unit-tests but nothing seems to work. My Code looks like this:

html:

<cdk-virtual-scroll-viewport tvsItemSize="48" style="height: 300px; width: 300px"

<table_ style="width: 100%; height: 300px" mat-table multiTemplateDataRows [dataSource]="dataSource" class="mat-elevation-z8"

<!-- Position Column -->
<ng-container matColumnDef="position">
  <th mat-header-cell *matHeaderCellDef>No.</th>
  <td mat-cell *matCellDef="let element">
    <div class="position">11{{ element.position }}</div>
  </td>
</ng-container>

<!-- Name Column -->
<ng-container matColumnDef="name">
  <th mat-header-cell *matHeaderCellDef>Name</th>
  <td mat-cell *matCellDef="let element">{{ element.name }}</td>
</ng-container>

<!-- Weight Column -->
<ng-container matColumnDef="weight">
  <th mat-header-cell *matHeaderCellDef>Weight</th>
  <td mat-cell *matCellDef="let element">{{ element.weight }}</td>
</ng-container>

<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
  <th mat-header-cell *matHeaderCellDef>Symbol</th>
  <td mat-cell *matCellDef="let element">{{ element.symbol }}</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>

<tr class="mat-row" *matNoDataRow>
  <td class="mat-cell" [attr.colspan]="4">No records found</td>
</tr>

code:

export interface PeriodicElement { name: string; position: number; weight: number; symbol: string; }

const ELEMENT_DATA: PeriodicElement[] = [...Array(1000).keys()].map((i) => ({ position: i + 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' }));

@Component({ selector: 'nipex-test-table', templateUrl: './test-table.component.html', styleUrls: ['./test-table.component.scss'] }) export class TestTableComponent implements OnInit { displayedColumns: string[] = ['position', 'name', 'weight', 'symbol']; dataSource: any;

constructor() {}

ngOnInit(): void { this.dataSource = new TableVirtualScrollDataSource( ELEMENT_DATA ); } }

spec:

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

const finishInit = () => { // On the first cycle we render and measure the viewport. fixture.detectChanges(); flush();

// On the second cycle we render the items.
fixture.detectChanges();
flush();

animationFrameScheduler.flush();
flush();
fixture.detectChanges();

};

beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [TestTableComponent], imports: [SharedModule, NoopAnimationsModule], schemas: [NO_ERRORS_SCHEMA], providers: [] }).compileComponents();

fixture = TestBed.createComponent(TestTableComponent);
component = fixture.componentInstance;
// fixture.detectChanges();

});

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

it('should render rows', fakeAsync(() => { finishInit();

// fixture.autoDetectChanges(); // <---
// tick(500);

const el = fixture.nativeElement as HTMLElement;

console.log(el.outerHTML);

const rowEls = Array.from(el.querySelectorAll('tbody tr')) as HTMLElement[];

expect(rowEls.length).toBe(3);

})); });

In the test, no rows are rendered. The "No record found" label is shown all the time. Any suggestion would be highly appreciated.

DamienBraillard commented 1 year ago

I found a trick that is working to render (at least some) rows while testing. This at least works for angular applications (I use Angular 12 as for now).

In your test.ts file add the following setup function:

// Ensure that the cdk-virtual-scroll-viewport is not fully squashed so that at least one row is rendered
beforeAll(() => {
  const styleElement = document.createElement('style');

  // Adjust min-width and min-height to display the amount of items you need (with 100px, at least one row should be shown)
  styleElement.textContent = 'cdk-virtual-scroll-viewport { min-width: 100px !important; min-height: 100px !important; }';
  styleElement.id = 'custom-test-style-virtual-scroll-viewport';

  document.head.append(styleElement);
});

This adds an extra style when testing. It ensures that the cdk-virtual-scroll-viewport wrapping the table has a minimum size. This causes the virtual scroll to render items (at least one in my case).

Note that I use 100px as I only provide one item when testing. You might want to increase min-width and/or min-height if needed.

For reference, this is my full test.ts file:

// This file is required by karma.conf.js and loads recursively all the .spec and framework files
// @formatter:off
import 'zone.js/dist/zone-testing'; // Keep this first otherwise tests will fail (https://stackoverflow.com/questions/67414283/error-zone-testing-js-is-needed-for-the-fakeasync-test-helper-but-could-not-b/70000591#70000591)
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
// @formatter:on

declare const require: {
  context(
    path: string,
    deep?: boolean,
    filter?: RegExp
  ): {
    keys(): string[];
    <T>(id: string): T;
  };
};

// Ensure that the cdk-virtual-scroll-viewport is not fully squashed so that at least one row is rendered
beforeAll(() => {
  const styleElement = document.createElement('style');

  // Adjust min-width and min-height to display the amount of items you need (with 100px, at least one row should be shown)
  styleElement.textContent = 'cdk-virtual-scroll-viewport { min-width: 100px !important; min-height: 100px !important; }';
  styleElement.id = 'custom-test-style-virtual-scroll-viewport';

  document.head.append(styleElement);
});

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

Hope this helps.

Zoliqa commented 1 year ago

Hi Damien. Thanks for your suggestion. I've tried it out but without any success. There are still no rows rendered in my case. It seems to me that the height of the table is not set and that could be the issue.

DamienBraillard commented 1 year ago

Hi Damien. Thanks for your suggestion. I've tried it out but without any success. There are still no rows rendered in my case. It seems to me that the height of the table is not set and that could be the issue.

Things you can try is to set the width and height of the <table> nested in the <cdk-virtual-scroll-viewport> to 100%.

Also, maybe try with the following setup CSS style:

div[id^=root] { width: 500px !important; height: 500px !important; overflow: scroll; opacity: 0; }

I had this as working global setup for tests before removing @angular\flex-layout and migrating ngx-datatable to material table.

beforeAll(() => {
  const styleElement = document.createElement('style');

  // Container of the tested component (A <div></div> with an id start starts with 'root'):
  //  - Set width to 500px x 500px
  //    This is particularly required for tables/grids that uses virtualization for their items.
  //    See: https://github.com/diprokon/ng-table-virtual-scroll/issues/103
  //  - Make it transparent and set overflow to scroll so that it does not screw up the test results display
  styleElement.textContent = 'div[id^=root] { width: 500px !important; height: 500px !important; overflow: scroll; opacity: 0; }';
  styleElement.id = 'custom-test-style-virtual-scroll-viewport';

  document.head.append(styleElement);
});

In the end, I had to find a style that ensured the size of the <cdk-virtual-scroll-viewport> element was not zero in any direction. To do so, I did put a breakpoint in one of the failing test, then I tried to see in chrome what CSS I could alter so that the row would render. That was tedious but it finally worked.

I really hope you find a solution. This has haunted me quite a bit.