From ce7c2365d6a036049920859027e481ea391635cf Mon Sep 17 00:00:00 2001 From: Galina Edinakova Date: Thu, 4 Dec 2025 13:48:33 +0200 Subject: [PATCH 1/4] chore(PDF): Adding a test verifying the exported child rows --- .../services/pdf/pdf-exporter-grid.spec.ts | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts index f41085540c2..07e47670301 100644 --- a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts @@ -6,14 +6,14 @@ import { GridIDNameJobTitleComponent } from '../../../../../test-utils/grid-samp import { first } from 'rxjs/operators'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NestedColumnGroupsGridComponent, ColumnGroupTestComponent, BlueWhaleGridComponent } from '../../../../../test-utils/grid-mch-sample.spec'; -import { IgxHierarchicalGridTestBaseComponent } from '../../../../../test-utils/hierarchical-grid-components.spec'; +import { IgxHierarchicalGridExportComponent, IgxHierarchicalGridTestBaseComponent } from '../../../../../test-utils/hierarchical-grid-components.spec'; import { IgxTreeGridSortingComponent, IgxTreeGridPrimaryForeignKeyComponent } from '../../../../../test-utils/tree-grid-components.spec'; import { CustomSummariesComponent } from 'igniteui-angular/grids/grid/src/grid-summary.spec'; import { IgxHierarchicalGridComponent } from 'igniteui-angular/grids/hierarchical-grid'; import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestComplexHierarchyComponent } from '../../../../../test-utils/pivot-grid-samples.spec'; import { IgxPivotGridComponent } from 'igniteui-angular/grids/pivot-grid'; import { PivotRowLayoutType } from 'igniteui-angular/grids/core'; -import { wait } from 'igniteui-angular/test-utils/ui-interactions.spec'; +import { UIInteractions, wait } from 'igniteui-angular/test-utils/ui-interactions.spec'; describe('PDF Grid Exporter', () => { let exporter: IgxPdfExporterService; @@ -35,7 +35,7 @@ describe('PDF Grid Exporter', () => { options = new IgxPdfExporterOptions('PdfGridExport'); // Spy the saveBlobToFile method so the files are not really created - spyOn(ExportUtilities as any, 'saveBlobToFile'); + // spyOn(ExportUtilities as any, 'saveBlobToFile'); }); it('should export grid as displayed.', (done) => { @@ -328,6 +328,50 @@ describe('PDF Grid Exporter', () => { exporter.export(grid, options); }); + fit('should export the correct number of child data rows from a hierarchical grid', (done) => { + const fix = TestBed.createComponent(IgxHierarchicalGridExportComponent); + fix.detectChanges(); + + const hGrid = fix.componentInstance.hGrid; + hGrid.data = hGrid.data.slice(0, 1); // Limit data for test performance + + // Expand first few rows to realize all inner levels, same as in Excel tests + const firstRow = hGrid.gridAPI.get_row_by_index(0) as any; + + UIInteractions.simulateClickAndSelectEvent(firstRow.expander); + fix.detectChanges(); + + let childGrids = hGrid.gridAPI.getChildGrids(false) as any[]; + const firstChildGrid = childGrids[0]; + const firstChildRow = firstChildGrid.gridAPI.get_row_by_index(0) as any; + const secondChildRow = firstChildGrid.gridAPI.get_row_by_index(1) as any; + UIInteractions.simulateClickAndSelectEvent(secondChildRow.expander); + fix.detectChanges(); + + UIInteractions.simulateClickAndSelectEvent(firstChildRow.expander); + fix.detectChanges(); + + childGrids = hGrid.gridAPI.getChildGrids(false) as any[]; + const thirdChildGrid = childGrids[1]; + const thirdChildRow = thirdChildGrid.gridAPI.get_row_by_index(0) as any; + UIInteractions.simulateClickAndSelectEvent(thirdChildRow.expander); + fix.detectChanges(); + + // Calculate expected number of data rows to be exported + const allGrids = [hGrid, ...(hGrid.gridAPI.getChildGrids(true) as any[])]; + const expectedRows = allGrids.reduce((acc, g) => acc + g.data.length, 0); + + // Spy PDF row drawing to count exported rows + const drawDataRowSpy = spyOn(exporter as any, 'drawDataRow').and.callThrough(); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(drawDataRowSpy.calls.count()).toBe(expectedRows); + done(); + }); + + exporter.export(hGrid, options); + }); + it('should export tree grid with hierarchical data', (done) => { TestBed.configureTestingModule({ imports: [ From 926191ed4a9d833e9d5e21979da9179185ac2b15 Mon Sep 17 00:00:00 2001 From: Galina Edinakova Date: Thu, 4 Dec 2025 13:49:08 +0200 Subject: [PATCH 2/4] chore(*): Removed the focus from the new test --- .../grids/core/src/services/pdf/pdf-exporter-grid.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts index 07e47670301..ef52a7de89e 100644 --- a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts @@ -328,7 +328,7 @@ describe('PDF Grid Exporter', () => { exporter.export(grid, options); }); - fit('should export the correct number of child data rows from a hierarchical grid', (done) => { + it('should export the correct number of child data rows from a hierarchical grid', (done) => { const fix = TestBed.createComponent(IgxHierarchicalGridExportComponent); fix.detectChanges(); From 517edce5e7a4318a9e752de4799aa693ec558730 Mon Sep 17 00:00:00 2001 From: Galina Edinakova Date: Thu, 4 Dec 2025 13:53:07 +0200 Subject: [PATCH 3/4] fix(*): Export proper child data on inner levels --- .../core/src/services/pdf/pdf-exporter.ts | 113 ++++++++++-------- 1 file changed, 61 insertions(+), 52 deletions(-) diff --git a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.ts b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.ts index 2b0c4299011..5e201538f10 100644 --- a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.ts +++ b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.ts @@ -347,28 +347,21 @@ export class IgxPdfExporterService extends IgxBaseExporter { // For hierarchical grids, check if this record has child records if (recordIsHierarchicalGrid) { - const allDescendants = []; + const allDescendants: Array = []; - // Collect all descendant records (children, grandchildren, etc.) that belong to this parent - // Child records have a different owner (island object) than the parent let j = i + 1; while (j < data.length && data[j].level > record.level) { - // Include all descendants (any level deeper) if (!data[j].hidden) { - allDescendants.push(data[j]); + // Attach the original index into data + allDescendants.push({ ...(data[j] as any), __index: j }); } j++; } - // If there are descendant records, draw child table(s) if (allDescendants.length > 0) { - // Group descendants by owner to separate different child grids - // Owner is the actual island object, not a string - // Only collect DIRECT children (one level deeper) for initial grouping - const directDescendantsByOwner = new Map(); + const directDescendantsByOwner = new Map>(); for (const desc of allDescendants) { - // Only include records that are exactly one level deeper (direct children) if (desc.level === record.level + 1) { const owner = desc.owner; if (!directDescendantsByOwner.has(owner)) { @@ -378,13 +371,12 @@ export class IgxPdfExporterService extends IgxBaseExporter { } } - // Draw each child grid separately with its direct children only for (const [owner, directChildren] of directDescendantsByOwner) { yPosition = this.drawHierarchicalChildren( pdf, data, - allDescendants, // Pass all descendants so grandchildren can be found - directChildren, + allDescendants, // descendants WITH __index + directChildren, // direct children WITH __index owner, yPosition, margin, @@ -397,7 +389,6 @@ export class IgxPdfExporterService extends IgxBaseExporter { ); } - // Skip the descendant records we just processed i = j - 1; } } @@ -644,7 +635,7 @@ export class IgxPdfExporterService extends IgxBaseExporter { private drawHierarchicalChildren( pdf: jsPDF, allData: IExportRecord[], - allDescendants: IExportRecord[], // All descendants to search for grandchildren + allDescendants: any[], // All descendants to search for grandchildren childRecords: IExportRecord[], // Direct children to render at this level childOwner: any, // Owner is the island object, not a string yPosition: number, @@ -768,49 +759,67 @@ export class IgxPdfExporterService extends IgxBaseExporter { this.drawDataRow(pdf, childRecord, childColumns, [], childTableX, yPosition, childColumnWidth, rowHeight, 0, options); yPosition += rowHeight; - // Check if this child has grandchildren (deeper levels in different child grids) - // Look for grandchildren in allDescendants that are direct descendants of this childRecord - const grandchildrenForThisRecord = allDescendants.filter(r => - r.level === childRecord.level + 1 && r.type !== 'HeaderRecord' - ); + // allDescendants here is an array of records with an extra __index property + const childIndex = (childRecord as any).__index as number | undefined; + + if (childIndex !== undefined) { + // Find this child's position in allDescendants (by original index) + const childPosInDesc = allDescendants.findIndex(d => d.__index === childIndex); - if (grandchildrenForThisRecord.length > 0) { - // Group grandchildren by their owner (different child islands under this record) - const grandchildrenByOwner = new Map(); - - for (const gc of grandchildrenForThisRecord) { - // Use the actual owner object - const gcOwner = gc.owner; - // Only include grandchildren that have a different owner (separate child grid) - if (gcOwner !== childOwner) { - if (!grandchildrenByOwner.has(gcOwner)) { - grandchildrenByOwner.set(gcOwner, []); + if (childPosInDesc !== -1) { + const subtree: Array = []; + const childLevel = childRecord.level; + + // Collect all deeper records until we hit same-or-higher level + for (let k = childPosInDesc + 1; k < allDescendants.length; k++) { + const rec = allDescendants[k]; + if (rec.level <= childLevel) { + break; + } + if (rec.type !== 'HeaderRecord') { + subtree.push(rec); } - grandchildrenByOwner.get(gcOwner)!.push(gc); } - } - // Recursively draw each grandchild owner's records with increased indentation - for (const [gcOwner, directGrandchildren] of grandchildrenByOwner) { - yPosition = this.drawHierarchicalChildren( - pdf, - allData, - allDescendants, // Pass all descendants so great-grandchildren can be found - directGrandchildren, // Direct grandchildren to render - gcOwner, - yPosition, - margin, - indentPerLevel + 20, // Increase indentation for next level - usableWidth, - pageHeight, - headerHeight, - rowHeight, - options - ); + if (subtree.length > 0) { + // Direct grandchildren for this child: exactly one level deeper + const grandchildrenForThisRecord = subtree.filter(r => + r.level === childRecord.level + 1 && r.owner !== childOwner + ); + + if (grandchildrenForThisRecord.length > 0) { + const grandchildrenByOwner = new Map>(); + + for (const gc of grandchildrenForThisRecord) { + const gcOwner = gc.owner; + if (!grandchildrenByOwner.has(gcOwner)) { + grandchildrenByOwner.set(gcOwner, []); + } + grandchildrenByOwner.get(gcOwner)!.push(gc); + } + + for (const [gcOwner, directGrandchildren] of grandchildrenByOwner) { + yPosition = this.drawHierarchicalChildren( + pdf, + allData, + subtree, // only this child's subtree for deeper levels + directGrandchildren, + gcOwner, + yPosition, + margin, + indentPerLevel + 20, + usableWidth, + pageHeight, + headerHeight, + rowHeight, + options + ); + } + } + } } } } - // Add spacing after child table yPosition += 5; From 6a51f40ad017341c62a006df3aee54d42474d13e Mon Sep 17 00:00:00 2001 From: Galina Edinakova Date: Thu, 4 Dec 2025 17:28:45 +0200 Subject: [PATCH 4/4] chore(*): Get the spy back. --- .../grids/core/src/services/pdf/pdf-exporter-grid.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts index ef52a7de89e..50488f19ee5 100644 --- a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts @@ -35,7 +35,7 @@ describe('PDF Grid Exporter', () => { options = new IgxPdfExporterOptions('PdfGridExport'); // Spy the saveBlobToFile method so the files are not really created - // spyOn(ExportUtilities as any, 'saveBlobToFile'); + spyOn(ExportUtilities as any, 'saveBlobToFile'); }); it('should export grid as displayed.', (done) => {