diff --git a/libs/angular-accelerator/package.json b/libs/angular-accelerator/package.json index a7627ae8..645f746e 100644 --- a/libs/angular-accelerator/package.json +++ b/libs/angular-accelerator/package.json @@ -14,6 +14,7 @@ "@onecx/integration-interface": "^5", "@onecx/angular-integration-interface": "^5", "@onecx/angular-remote-components": "^5", + "@onecx/angular-testing": "^5", "chart.js": "^4.4.3", "d3-scale-chromatic": "^3.1.0", "rxjs": "~7.8.1", diff --git a/libs/angular-accelerator/src/lib/components/data-list-grid/data-list-grid.component.html b/libs/angular-accelerator/src/lib/components/data-list-grid/data-list-grid.component.html index 41f4fe6a..501268a1 100644 --- a/libs/angular-accelerator/src/lib/components/data-list-grid/data-list-grid.component.html +++ b/libs/angular-accelerator/src/lib/components/data-list-grid/data-list-grid.component.html @@ -27,6 +27,7 @@
+ @defer (on viewport; on idle){ + } @placeholder { +
+ }
@@ -87,7 +91,7 @@ -
+
@@ -190,16 +194,19 @@ [ocxTooltipOnOverflow]="col.columnType === columnType.TRANSLATION_KEY ? (resolveFieldData(item,col.id) | translate) : resolveFieldData(item, col.id)" tooltipPosition="top" > + @defer(on viewport;){ + }" + > + + } @placeholder { + + }
@@ -214,6 +221,7 @@ - + + @defer(on viewport){ + > + + } @placeholder { + + } diff --git a/libs/angular-accelerator/src/lib/components/data-table/data-table.component.ts b/libs/angular-accelerator/src/lib/components/data-table/data-table.component.ts index 7fc1a518..3d0bf732 100644 --- a/libs/angular-accelerator/src/lib/components/data-table/data-table.component.ts +++ b/libs/angular-accelerator/src/lib/components/data-table/data-table.component.ts @@ -137,7 +137,8 @@ export class DataTableComponent extends DataSortBase implements OnInit, AfterCon const obs = value.map((c) => this.getTemplate(c, TemplateType.CELL)) const filterObs = value.map((c) => this.getTemplate(c, TemplateType.FILTERCELL)) this.columnTemplates$ = combineLatest(obs).pipe( - map((values) => Object.fromEntries(value.map((c, i) => [c.id, values[i]]))) + map((values) => Object.fromEntries(value.map((c, i) => [c.id, values[i]]))), + debounceTime(50) ) this.columnFilterTemplates$ = combineLatest(filterObs).pipe( map((values) => Object.fromEntries(value.map((c, i) => [c.id, values[i]]))) @@ -908,8 +909,7 @@ export class DataTableComponent extends DataSortBase implements OnInit, AfterCon return columnTemplate } return this.getColumnTypeTemplate(templates, column.columnType, templateType) - }), - debounceTime(50) + }) ) } return templatesData.templatesObservables[column.id] diff --git a/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.spec.ts b/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.spec.ts index 59dfe9ac..b1aff25a 100644 --- a/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.spec.ts +++ b/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.spec.ts @@ -307,6 +307,8 @@ describe('InteractiveDataViewComponent', () => { viewItemEvent = undefined editItemEvent = undefined deleteItemEvent = undefined + + console.log("Global IntersectionObserver", global.IntersectionObserver) }) it('should create', () => { @@ -433,7 +435,6 @@ describe('InteractiveDataViewComponent', () => { ] const sortButton = await tableHeaders[0].getSortButton() await sortButton.click() - tableRows = (await dataTable?.getRows()) ?? [] const rows = await parallel(() => tableRows.map((row) => row.getData())) diff --git a/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.stories.ts b/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.stories.ts index fa0c6a8f..ad14111f 100644 --- a/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.stories.ts +++ b/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.stories.ts @@ -38,6 +38,8 @@ import { of } from 'rxjs' import { Filter, FilterType } from '../../model/filter.model' import { FilterViewComponent } from '../filter-view/filter-view.component' import { FocusTrapModule } from 'primeng/focustrap' +import { TooltipOnOverflowDirective } from '../../directives/tooltipOnOverflow.directive' +import { SkeletonModule } from 'primeng/skeleton' type InteractiveDataViewInputTypes = Pick< InteractiveDataViewComponent, @@ -85,6 +87,7 @@ const InteractiveDataViewComponentSBConfig: Meta = DataListGridComponent, DataListGridSortingComponent, FilterViewComponent, + TooltipOnOverflowDirective ], imports: [ TableModule, @@ -105,6 +108,7 @@ const InteractiveDataViewComponentSBConfig: Meta = OverlayPanelModule, FocusTrapModule, ChipModule, + SkeletonModule ], }), ], @@ -192,6 +196,71 @@ export const WithMockData = { }, } +function generateColumns(count: number) { + const data = [] + for (let i = 0; i < count; i++) { + const row = { + id: `${i + 1}`, + columnType: ColumnType.STRING, + nameKey: `Product${i + 1}`, + sortable: false, + filterable: true, + predefinedGroupKeys: ['test'], + }; + data.push(row) + } + return data +} + +function generateRows(rowCount: number, columnCount: number) { + const data = [] + for (let i = 0; i < rowCount; i++) { + const row = {} as any + for(let j = 0; j < columnCount; j++) { + row[j+1] = `Test value for ${j+1}` + } + data.push(row) + } + return data +} + +function generateColumnTemplates(columnCount: number) { + let templates = '' + for(let i = 0; i < columnCount; i++) { + templates += ` + + ${i+1} {{ rowObject[${i+1}] }} + ` + } + return templates +} + +const columnCount = 30; +const rowCount = 500 + +const HugeMockDataTemplate: StoryFn = (args) => ({ + props: args, + template: ` + + ${generateColumnTemplates(Math.ceil(columnCount/3))} + `, +}) + +export const WithHugeMockData = { + argTypes: { + componentStateChanged: { action: 'componentStateChanged' }, + selectionChanged: { action: 'selectionChanged' }, + }, + render: HugeMockDataTemplate, + args: { + columns: generateColumns(columnCount), + data: generateRows(rowCount, columnCount), + emptyResultsMessage: 'No results', + selectedRows: [], + pageSize: 50, + }, +} + export const WithPageSizes = { argTypes: { componentStateChanged: { action: 'componentStateChanged' }, diff --git a/libs/angular-accelerator/testing/data-list-grid.harness.ts b/libs/angular-accelerator/testing/data-list-grid.harness.ts index 576fcb0d..8463c73d 100644 --- a/libs/angular-accelerator/testing/data-list-grid.harness.ts +++ b/libs/angular-accelerator/testing/data-list-grid.harness.ts @@ -2,15 +2,20 @@ import { ContentContainerComponentHarness, TestElement, parallel } from '@angula import { PPaginatorHarness } from '@onecx/angular-testing' import { DefaultGridItemHarness } from './default-grid-item.harness' import { DefaultListItemHarness } from './default-list-item.harness' +import { waitForDeferredViewsToBeRendered } from '@onecx/angular-testing' export class DataListGridHarness extends ContentContainerComponentHarness { static hostSelector = 'ocx-data-list-grid' getDefaultGridItems = this.locatorForAll(DefaultGridItemHarness) - getDefaultListItems = this.locatorForAll(DefaultListItemHarness) getPaginator = this.locatorFor(PPaginatorHarness) getMenuButton = this.locatorFor(`[name="data-grid-item-menu-button"]`) + async getDefaultListItems() { + await waitForDeferredViewsToBeRendered(this) + return await this.locatorForAll(DefaultListItemHarness)() + } + async getActionButtons(actionButtonType: 'list' | 'grid' | 'grid-hidden') { if (actionButtonType === 'list') { return await this.locatorForAll(`[name="data-list-action-button"]`)() diff --git a/libs/angular-accelerator/testing/default-list-item.harness.ts b/libs/angular-accelerator/testing/default-list-item.harness.ts index 3e011086..3e5c2f8a 100644 --- a/libs/angular-accelerator/testing/default-list-item.harness.ts +++ b/libs/angular-accelerator/testing/default-list-item.harness.ts @@ -1,5 +1,6 @@ import { ComponentHarness } from '@angular/cdk/testing' import { ButtonHarness, DivHarness } from '@onecx/angular-testing' +import { waitForDeferredViewsToBeRendered } from '@onecx/angular-testing' export class DefaultListItemHarness extends ComponentHarness { static hostSelector = '.data-list-items' @@ -12,6 +13,7 @@ export class DefaultListItemHarness extends ComponentHarness { private getAllDivs = this.locatorForAll(DivHarness) async getData() { + await waitForDeferredViewsToBeRendered(this) const isDataListItemsDiv = await Promise.all( (await this.getAllDivs()).map((innerDivHarness) => this.checkDivsHasClasses(innerDivHarness)) ) diff --git a/libs/angular-accelerator/testing/index.ts b/libs/angular-accelerator/testing/index.ts index b1968f74..6872aaff 100644 --- a/libs/angular-accelerator/testing/index.ts +++ b/libs/angular-accelerator/testing/index.ts @@ -1,3 +1,5 @@ +import { ensureIntersectionObserverMockExists } from '@onecx/angular-testing' + export * from './column-group-selection.harness' export * from './custom-group-column-selector.harness' export * from './data-layout-selection.harness' @@ -18,3 +20,7 @@ export * from './search-header.harness' export * from '@angular/cdk/testing' export * from '@angular/cdk/testing/testbed' export * from '@onecx/angular-testing' + +ensureIntersectionObserverMockExists() +declare let global: any +global.origin = '' \ No newline at end of file diff --git a/libs/angular-testing/src/index.ts b/libs/angular-testing/src/index.ts index 08577358..f8b76e9c 100644 --- a/libs/angular-testing/src/index.ts +++ b/libs/angular-testing/src/index.ts @@ -36,3 +36,6 @@ export * from './lib/harnesses/table-row.harness' export * from '@angular/cdk/testing' export * from '@angular/cdk/testing/testbed' + +export * from './lib/mocks/IntersectionObserverMock' +export * from './lib/utils/waitForDeferredViewsToBeRendered' diff --git a/libs/angular-testing/src/lib/harnesses/button.harness.ts b/libs/angular-testing/src/lib/harnesses/button.harness.ts index 016dabf4..3f7c3eac 100644 --- a/libs/angular-testing/src/lib/harnesses/button.harness.ts +++ b/libs/angular-testing/src/lib/harnesses/button.harness.ts @@ -32,6 +32,7 @@ export class ButtonHarness extends ComponentHarness { } else { console.warn('Button cannot be clicked, because it is disabled!') } + await this.waitForTasksOutsideAngular() } async isDisabled(): Promise { diff --git a/libs/angular-testing/src/lib/harnesses/table-row.harness.ts b/libs/angular-testing/src/lib/harnesses/table-row.harness.ts index e15e803d..e6b8b107 100644 --- a/libs/angular-testing/src/lib/harnesses/table-row.harness.ts +++ b/libs/angular-testing/src/lib/harnesses/table-row.harness.ts @@ -1,5 +1,6 @@ import { ContentContainerComponentHarness } from '@angular/cdk/testing' import { ButtonHarness } from './button.harness' +import { waitForDeferredViewsToBeRendered } from '../utils/waitForDeferredViewsToBeRendered' export class TableRowHarness extends ContentContainerComponentHarness { static hostSelector = 'tbody > tr' @@ -10,6 +11,7 @@ export class TableRowHarness extends ContentContainerComponentHarness { getDeleteButton = this.locatorForOptional(ButtonHarness.with({ class: 'deleteTableRowButton' })) async getData(): Promise { + await waitForDeferredViewsToBeRendered(this) const tds = await this.locatorForAll('td')() const isActionsTd = await Promise.all(tds.map((t) => t.hasClass('actions'))) const textTds = tds.filter((_v, index) => !isActionsTd[index]) diff --git a/libs/angular-testing/src/lib/mocks/IntersectionObserverMock.ts b/libs/angular-testing/src/lib/mocks/IntersectionObserverMock.ts new file mode 100644 index 00000000..7e862a8c --- /dev/null +++ b/libs/angular-testing/src/lib/mocks/IntersectionObserverMock.ts @@ -0,0 +1,42 @@ +export class IntersectionObserverMock { + private callback: any + private entries: any[] + root: any + rootMargin: any + thresholds: any + constructor(callback: any, _options: any) { + this.callback = callback + this.entries = [] + } + + observe(target: Element) { + const entry = { + boundingClientRect: target.getBoundingClientRect(), + intersectionRatio: 1, + isIntersecting: true, + target: target, + } + this.entries.push(entry) + setTimeout(() => { + this.callback(this.entries, this) + }) + } + + takeRecords() { + return this.entries + } + + unobserve(target: any) { + this.entries = this.entries.filter((entry) => entry.target !== target) + } + + disconnect() { + this.entries = [] + } +} + +export function ensureIntersectionObserverMockExists() { + if (!global.IntersectionObserver || global.IntersectionObserver !== IntersectionObserverMock) { + global.IntersectionObserver = IntersectionObserverMock + } +} diff --git a/libs/angular-testing/src/lib/utils/waitForDeferredViewsToBeRendered.ts b/libs/angular-testing/src/lib/utils/waitForDeferredViewsToBeRendered.ts new file mode 100644 index 00000000..23010b6f --- /dev/null +++ b/libs/angular-testing/src/lib/utils/waitForDeferredViewsToBeRendered.ts @@ -0,0 +1,17 @@ +import { ComponentHarness, ContentContainerComponentHarness } from '@angular/cdk/testing' + +export async function waitForDeferredViewsToBeRendered(harness: ComponentHarness | ContentContainerComponentHarness) { + return await new Promise((resolve) => { + setTimeout(() => { + console.warn( + 'waitForTasksOutsideAngular has not finished within 500ms. We are not waiting any longer to not cause timeouts.' + ); + (harness as any).forceStabilize().then(() => resolve()) + }, 500) + // waitForTasksOutsideAngular makes sure that the observe method of the IntersectionObserver is called for each defer block. + // setTimeout makes sure that we are only continuing after the IntersectionObserverMock has called ther callback for each + // defer block, because js scheduling is making sure that all methods which are scheduled via setTimeout are executed in the + // respective order. This guarentees that the resolve method is called after the defer block was rendered. + ;(harness as any).waitForTasksOutsideAngular().then(() => setTimeout(() => resolve())) + }) +} diff --git a/libs/angular-testing/tsconfig.json b/libs/angular-testing/tsconfig.json index cd40c79f..5513af1b 100644 --- a/libs/angular-testing/tsconfig.json +++ b/libs/angular-testing/tsconfig.json @@ -7,7 +7,8 @@ "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "esModuleInterop": true + "esModuleInterop": true, + "types": ["jest", "node"], }, "files": [], "include": [], diff --git a/libs/angular-testing/tsconfig.lib.json b/libs/angular-testing/tsconfig.lib.json index b9561ca9..33af08a4 100644 --- a/libs/angular-testing/tsconfig.lib.json +++ b/libs/angular-testing/tsconfig.lib.json @@ -5,7 +5,6 @@ "declaration": true, "declarationMap": true, "inlineSources": true, - "types": [], "target": "es2022", "useDefineForClassFields": false }, diff --git a/package-lock.json b/package-lock.json index 81101900..10807c90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@onecx/onecx-portal-ui-libs", - "version": "5.13.0", + "version": "5.23.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@onecx/onecx-portal-ui-libs", - "version": "5.13.0", + "version": "5.23.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -121,7 +121,7 @@ }, "libs/portal-layout-styles": { "name": "@onecx/portal-layout-styles", - "version": "5.13.0", + "version": "5.23.3", "license": "Apache-2.0", "peerDependencies": { "tslib": "^2.6.3"