diff --git a/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.html b/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.html index a05110ed..cb9d8ab7 100644 --- a/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.html +++ b/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.html @@ -1,4 +1,4 @@ -
+
{{ subheader }} (onClick)="action.actionCallback()" [title]="(action.titleKey ? (action.titleKey | translate) : action.title) || (action.labelKey ? (action.labelKey | translate) : action.label)" [disabled]="action.disabled ? action.disabled : false" - [attr.data-testid]="action.icon ? 'ocx-page-header-inline-action-icon-button' : 'ocx-page-header-inline-action-button'" + [attr.name]="action.icon ? 'ocx-page-header-inline-action-icon-button' : 'ocx-page-header-inline-action-button'" >
@@ -62,7 +62,7 @@

{{ subheader }}

title="{{ 'OCX_PAGE_HEADER.MORE_ACTIONS' | translate }}" class="more-actions-menu-button action-button ml-2" (click)="menu.toggle($event)" - data-testid="ocx-page-header-overflow-action-button" + name="ocx-page-header-overflow-action-button" >
@@ -82,9 +82,16 @@

{{ subheader }}

class="object-info flex flex-row md:flex-column align-items-baseline md:align-items-center justify-content-between" *ngFor="let item of objectDetails" > - - {{ item.value | dynamicPipe:item.valuePipe:item.valuePipeArgs}}{{ item.label | dynamicPipe:item.labelPipe }} + + + {{ item.value | dynamicPipe:item.valuePipe:item.valuePipeArgs}} diff --git a/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.spec.ts b/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.spec.ts index 287a6931..ebb3f250 100644 --- a/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.spec.ts +++ b/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.spec.ts @@ -3,7 +3,6 @@ import { Action, PageHeaderComponent } from './page-header.component' import { RouterTestingModule } from '@angular/router/testing' import { ConfigurationService } from '../../../services/configuration.service' import { HttpClientTestingModule } from '@angular/common/http/testing' -import { Component } from '@angular/core' import { TranslateTestingModule } from 'ngx-translate-testing' import { BreadcrumbModule } from 'primeng/breadcrumb' import { MenuModule } from 'primeng/menu' @@ -11,6 +10,9 @@ import { ButtonModule } from 'primeng/button' import { AppStateService } from '../../../services/app-state.service' import { UserService } from '../../../services/user.service' import { MockUserService } from '../../../../../mocks/mock-user-service' +import { PageHeaderHarness, TestbedHarnessEnvironment } from '../../../../../testing' +import { DynamicPipe } from '../../pipes/dynamic.pipe' +import { PrimeIcons } from 'primeng/api' import { NoopAnimationsModule } from '@angular/platform-browser/animations' const mockActions: Action[] = [ @@ -41,15 +43,6 @@ const mockActions: Action[] = [ }, ] -// Mock host component that's used in our testBed instead of ocx-page-header -// Using this mock host allows us to simulate Angular @Input mechanisms -@Component({ - template: '', -}) -class TestHostComponent { - actions: Action[] | undefined -} - describe('PageHeaderComponent', () => { const origAddEventListener = window.addEventListener const origPostMessage = window.postMessage @@ -73,13 +66,14 @@ describe('PageHeaderComponent', () => { window.postMessage = origPostMessage }) - let component: TestHostComponent - let fixture: ComponentFixture + let component: PageHeaderComponent + let fixture: ComponentFixture + let pageHeaderHarness: PageHeaderHarness let userServiceSpy: jest.SpyInstance beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [PageHeaderComponent, TestHostComponent], + declarations: [PageHeaderComponent, PageHeaderComponent, DynamicPipe], imports: [ RouterTestingModule, HttpClientTestingModule, @@ -104,124 +98,197 @@ describe('PageHeaderComponent', () => { }) }) - beforeEach(() => { - fixture = TestBed.createComponent(TestHostComponent) + beforeEach(async () => { + fixture = TestBed.createComponent(PageHeaderComponent) component = fixture.componentInstance fixture.detectChanges() + pageHeaderHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, PageHeaderHarness) const userService = fixture.debugElement.injector.get(UserService) jest.restoreAllMocks() userServiceSpy = jest.spyOn(userService, 'hasPermission') }) - it('should create', () => { + it('should create', async () => { expect(component).toBeTruthy() - expect(fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-wrapper"]')).toHaveLength( - 1 - ) + const pageHeaderWrapper = await pageHeaderHarness.getPageHeaderWrapperHarness() + expect(pageHeaderWrapper).toBeTruthy() }) - it('should check permissions and render buttons accordingly', () => { + it('should check permissions and render buttons accordingly', async () => { expect( - fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-inline-action-button"]') + await pageHeaderHarness.getInlineActionButtons() ).toHaveLength(0) expect( - fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-overflow-action-button"]') + await pageHeaderHarness.getOverflowActionButtons() ).toHaveLength(0) component.actions = mockActions - fixture.detectChanges() expect( - fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-inline-action-button"]') + await pageHeaderHarness.getInlineActionButtons() ).toHaveLength(1) - expect(fixture.debugElement.nativeElement.querySelector('[title="My Test Action"]')).toBeTruthy() + expect(await pageHeaderHarness.getElementByTitle('My Test Action')).toBeTruthy() expect( - fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-overflow-action-button"]') + await pageHeaderHarness.getOverflowActionButtons() ).toHaveLength(1) - expect(fixture.debugElement.nativeElement.querySelector('[title="More actions"]')).toBeTruthy() - expect(userServiceSpy).toHaveBeenCalledTimes(6) + expect(await pageHeaderHarness.getElementByTitle('More actions')).toBeTruthy() + expect(userServiceSpy).toHaveBeenCalledTimes(3) }) - it("should check permissions and not render button that user isn't allowed to see", () => { + it("should check permissions and not render button that user isn't allowed to see", async () => { userServiceSpy.mockClear() userServiceSpy.mockReturnValue(false) expect( - fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-inline-action-button"]') + await pageHeaderHarness.getInlineActionButtons() ).toHaveLength(0) expect( - fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-overflow-action-button"]') + await pageHeaderHarness.getOverflowActionButtons() ).toHaveLength(0) component.actions = mockActions - fixture.detectChanges() expect( - fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-inline-action-button"]') + await pageHeaderHarness.getInlineActionButtons() ).toHaveLength(0) - expect(fixture.debugElement.nativeElement.querySelector('[title="My Test Action"]')).toBeFalsy() + expect(await pageHeaderHarness.getElementByTitle('My Test Action')).toBeFalsy() expect( - fixture.debugElement.nativeElement.querySelectorAll('[data-testid="ocx-page-header-overflow-action-button"]') + await pageHeaderHarness.getOverflowActionButtons() ).toHaveLength(0) - expect(fixture.debugElement.nativeElement.querySelector('[title="More actions"]')).toBeFalsy() - expect(userServiceSpy).toHaveBeenCalledTimes(6) + expect(await pageHeaderHarness.getElementByTitle('More actions')).toBeFalsy() + expect(userServiceSpy).toHaveBeenCalledTimes(3) }) - it('should show overflow actions when menu overflow button clicked', () => { + it("should render objectDetails as object info in the page header", async () => { + const objectDetailsWithoutIcons = [ + { + label: 'Venue', + value: 'AIE Munich', + }, + { + label: 'Status', + value: 'Confirmed', + }, + ] + expect((await pageHeaderHarness.getObjectInfos()).length).toEqual(0) + + component.objectDetails = objectDetailsWithoutIcons + + expect((await pageHeaderHarness.getObjectInfos()).length).toEqual(2) + const objectDetailLabels = await pageHeaderHarness.getObjectDetailLabels() + const objectDetailValues = await pageHeaderHarness.getObjectDetailValues() + const objectDetailIcons = await pageHeaderHarness.getObjectDetailIcons() + expect(objectDetailLabels.length).toEqual(2) + expect(objectDetailValues.length).toEqual(2) + expect(objectDetailIcons.length).toEqual(0) + + objectDetailLabels.forEach(async (label, i) => { + expect(await label.text()).toEqual(objectDetailsWithoutIcons[i].label) + }) + + objectDetailValues.forEach(async (value, i) => { + expect(await value.text()).toEqual(objectDetailsWithoutIcons[i].value) + }) + }) + + it("should render objectDetails with icons as object info in the page header", async () => { + const objectDetailsWithIcons = [ + { + label: 'Venue', + value: 'AIE Munich', + }, + { + label: 'Status', + value: 'Confirmed', + icon: PrimeIcons.CHECK + }, + { + label: 'Done?', + icon: PrimeIcons.EXCLAMATION_CIRCLE + }, + { + label: 'Empty' + } + ] + expect((await pageHeaderHarness.getObjectInfos()).length).toEqual(0) + + component.objectDetails = objectDetailsWithIcons + + expect((await pageHeaderHarness.getObjectInfos()).length).toEqual(4) + const objectDetailLabels = await pageHeaderHarness.getObjectDetailLabels() + const objectDetailValues = await pageHeaderHarness.getObjectDetailValues() + const objectDetailIcons = await pageHeaderHarness.getObjectDetailIcons() + expect(objectDetailLabels.length).toEqual(4) + expect(objectDetailValues.length).toEqual(3) + expect(objectDetailIcons.length).toEqual(2) + + objectDetailLabels.forEach(async (label, i) => { + expect(await label.text()).toEqual(objectDetailsWithIcons[i].label) + }) + + objectDetailValues.forEach(async (value, i) => { + if(objectDetailsWithIcons[i].value) { + expect(await value.text()).toEqual(objectDetailsWithIcons[i].value) + } + }) + + expect(await objectDetailIcons[0].getAttribute('class')).toEqual(objectDetailsWithIcons[1].icon) + expect(await objectDetailIcons[1].getAttribute('class')).toEqual(objectDetailsWithIcons[2].icon) + }) + + it('should show overflow actions when menu overflow button clicked', async () => { component.actions = mockActions - fixture.detectChanges() - const menuOverflowButton = fixture.debugElement.nativeElement.querySelector( - '[data-testid="ocx-page-header-overflow-action-button"]' - ) - expect(menuOverflowButton).toBeTruthy() - menuOverflowButton.click() - fixture.detectChanges() + const menuOverflowButtons = await pageHeaderHarness.getOverflowActionButtons() + + expect(menuOverflowButtons).toBeTruthy() + expect(menuOverflowButtons.length).toBe(1) + await menuOverflowButtons[0].click() - expect(fixture.debugElement.nativeElement.querySelector('[title="My Test Overflow Action"]')).toBeTruthy() - expect(fixture.debugElement.nativeElement.querySelector('[title="My Test Overflow Disabled Action"]')).toBeTruthy() + const menuItems = await pageHeaderHarness.getOverFlowMenuItems() + expect(menuItems.length).toBe(2) + expect(await menuItems[0].getText()).toBe("My Test Overflow Action") + expect(await menuItems[1].getText()).toBe("My Test Overflow Disabled Action") }) - it('should use provided action callback on overflow button click', () => { + it('should use provided action callback on overflow button click', async () => { jest.spyOn(console, 'log') component.actions = mockActions - fixture.detectChanges() - const menuOverflowButton = fixture.debugElement.nativeElement.querySelector( - '[data-testid="ocx-page-header-overflow-action-button"]' - ) - expect(menuOverflowButton).toBeTruthy() - menuOverflowButton.click() - fixture.detectChanges() + const menuOverflowButtons = await pageHeaderHarness.getOverflowActionButtons() + + expect(menuOverflowButtons).toBeTruthy() + expect(menuOverflowButtons.length).toBe(1) + await menuOverflowButtons[0].click() - const enabledActionElement = fixture.debugElement.nativeElement.querySelector('[title="My Test Overflow Action"]') - expect(enabledActionElement).toBeTruthy() - expect(enabledActionElement.classList).not.toContain('p-disabled') - enabledActionElement.click() + const menuItems = await pageHeaderHarness.getOverFlowMenuItems() + expect(menuItems.length).toBe(2) + const enabledActionElement = await menuItems[0].host() + expect(await(enabledActionElement.hasClass('p-disabled'))).toBe(false) + await enabledActionElement.click() expect(console.log).toHaveBeenCalledTimes(1) }) - it('should disable overflow button when action is disabled', () => { + it('should disable overflow button when action is disabled', async () => { jest.spyOn(console, 'log') component.actions = mockActions - fixture.detectChanges() - const menuOverflowButton = fixture.debugElement.nativeElement.querySelector( - '[data-testid="ocx-page-header-overflow-action-button"]' - ) + const menuOverflowButton = await pageHeaderHarness.getOverflowActionButtons() expect(menuOverflowButton).toBeTruthy() - menuOverflowButton.click() - fixture.detectChanges() + expect(menuOverflowButton.length).toBe(1) + menuOverflowButton[0].click() - const disabledActionElement = fixture.debugElement.nativeElement.querySelector( - '[title="My Test Overflow Disabled Action"]' - ) + const overFlowMenuItems = await pageHeaderHarness.getOverFlowMenuItems() + const disabledActionElement = overFlowMenuItems[1] + + expect(overFlowMenuItems).toBeTruthy() + expect(overFlowMenuItems?.length).toBe(2) expect(disabledActionElement).toBeTruthy() - expect(disabledActionElement.classList).toContain('p-disabled') - disabledActionElement.click() + expect(await (await disabledActionElement.host()).hasClass('p-disabled')).toBe(true) + await (await disabledActionElement.host()).click() expect(console.log).toHaveBeenCalledTimes(0) }) }) diff --git a/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.stories.ts b/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.stories.ts index d082f453..78ea4342 100644 --- a/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.stories.ts +++ b/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.stories.ts @@ -17,20 +17,14 @@ import { BreadcrumbService } from '../../../services/breadcrumb.service' import { DynamicPipe } from '../../pipes/dynamic.pipe' import { Action, ObjectDetailItem, PageHeaderComponent } from './page-header.component' import { HttpClientModule } from '@angular/common/http' -import { AppStateService } from '../../../services/app-state.service' +import { PrimeIcons } from 'primeng/api' -function initFactory(breadcrumbService: BreadcrumbService, appStateService: AppStateService) { +function initFactory(breadcrumbService: BreadcrumbService) { return async () => { breadcrumbService.setItems([ { label: 'Level 1', routerLink: 'something' }, { label: 'Level 2', url: '/' }, ]) - await appStateService.currentPortal$.publish({ - baseUrl: '/demo', - portalName: 'Demo', - id: 'Demo', - microfrontendRegistrations: [], - }) } } @@ -300,3 +294,61 @@ export const WithCustomContent = { objectDetails: demoFields, }, } + +const objectDetailsWithoutIcons: ObjectDetailItem[] = [ + { + label: 'Venue', + value: 'AIE Munich ', + }, + { + label: 'Status', + value: 'Confirmed', + }, + { + label: 'Start Date', + value: '14.3.2022', + }, +] + +export const WithObjectDetails = { + render: Template, + + args: { + header: 'Test Page', + subheader: 'Page header with text based objectDetails and no icons', + loading: false, + objectDetails: objectDetailsWithoutIcons, + showBreadcrumbs: false, + }, +} + +const objectDetailsWithIcons: ObjectDetailItem[] = [ + { + label: 'Venue', + value: 'AIE Munich ', + }, + { + label: 'Event Completed', + icon: PrimeIcons.CHECK_CIRCLE + }, + { + label: 'Start Date', + value: '14.3.2022', + icon: PrimeIcons.CLOCK + }, + { + label: 'I have no value' + }, +] + +export const WithObjectDetailsAndIcons = { + render: Template, + + args: { + header: 'Test Page', + subheader: 'Page header with text and icon based objectDetails', + loading: false, + objectDetails: objectDetailsWithIcons, + showBreadcrumbs: false, + }, +} \ No newline at end of file diff --git a/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.ts b/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.ts index 08c93167..490c8404 100644 --- a/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.ts +++ b/libs/portal-integration-angular/src/lib/core/components/page-header/page-header.component.ts @@ -11,7 +11,7 @@ import { Type, ViewEncapsulation, } from '@angular/core' -import { MenuItem } from 'primeng/api' +import { MenuItem, PrimeIcons } from 'primeng/api' import { concat, map, Observable, of } from 'rxjs' import { BreadcrumbService } from '../../../services/breadcrumb.service' import { TranslateService } from '@ngx-translate/core' @@ -46,6 +46,7 @@ export interface ObjectDetailItem { label: string value?: string tooltip?: string + icon?: PrimeIcons labelPipe?: Type valuePipe?: Type valuePipeArgs?: string diff --git a/libs/portal-integration-angular/testing/index.ts b/libs/portal-integration-angular/testing/index.ts index b18f4551..4ff94656 100644 --- a/libs/portal-integration-angular/testing/index.ts +++ b/libs/portal-integration-angular/testing/index.ts @@ -31,6 +31,7 @@ export * from './table-row.harness' export * from './diagram.harness' export * from './search-config.harness' export * from './span.harness' +export * from './page-header.harness' export * from './p-tableCheckbox.harness' export * from '@angular/cdk/testing' diff --git a/libs/portal-integration-angular/testing/page-header.harness.ts b/libs/portal-integration-angular/testing/page-header.harness.ts new file mode 100644 index 00000000..709ffbc8 --- /dev/null +++ b/libs/portal-integration-angular/testing/page-header.harness.ts @@ -0,0 +1,42 @@ +import { ComponentHarness } from "@angular/cdk/testing"; +import { PMenuHarness } from "./primeng/p-menu.harness"; + +export class PageHeaderHarness extends ComponentHarness { + static hostSelector = 'ocx-page-header' + + getPageHeaderWrapperHarness = this.locatorForAll('[name="ocx-page-header-wrapper"]') + + async getInlineActionButtons() { + return await this.locatorForAll('[name="ocx-page-header-inline-action-button"]')() + } + + async getOverflowActionButtons() { + return await this.locatorForAll('[name="ocx-page-header-overflow-action-button"]')() + } + + async getElementByTitle(title: string) { + return await this.locatorForOptional(`[title="${title}"]`)() + } + + async getObjectInfos() { + return await this.locatorForAll('.object-info')() + } + + async getObjectDetailLabels() { + return await this.locatorForAll('[name="object-detail-label"]')() + } + + async getObjectDetailValues() { + return await this.locatorForAll('[name="object-detail-value"]')() + } + + async getObjectDetailIcons() { + return await this.locatorForAll('[name="object-detail-icon"]')() + } + + async getOverFlowMenuItems() { + const menu = await this.locatorFor(PMenuHarness)() + const menuItems = await menu.getAllMenuItems() + return menuItems ?? [] + } +} \ No newline at end of file