From cc39b19eea02c7c5b46f15d1c6542ccd99ff1287 Mon Sep 17 00:00:00 2001 From: Ehsan Rezaei Date: Wed, 19 Aug 2026 22:33:38 +0200 Subject: [PATCH] AAE-50665 Handling dynamic component destroy (#12183) * AAE-50665 Handling dynamic component destroy * AAE-50665 Code improvements --- .../base-screen-cloud.component.spec.ts | 198 ++++++++++++++++++ .../base-screen-cloud.component.ts | 38 ++-- ...art-process-screen-cloud.component.spec.ts | 124 ++++++++++- .../start-process-screen-cloud.component.ts | 10 +- .../screen-cloud.component.spec.ts | 126 ++++++++++- .../screen-cloud.component.ts | 78 +++---- 6 files changed, 507 insertions(+), 67 deletions(-) create mode 100644 lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.spec.ts diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.spec.ts new file mode 100644 index 0000000000..f1135da80e --- /dev/null +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.spec.ts @@ -0,0 +1,198 @@ +/*! + * @license + * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, ComponentRef, OnDestroy } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BaseScreenCloudComponent } from './base-screen-cloud.component'; +import { provideScreen } from '../../../services/provide-screen'; +import { ScreenRenderingService } from '../../../services/screen-rendering.service'; + +@Component({ + selector: 'adf-cloud-test-dynamic-screen', + template: `
dynamic screen
` +}) +class TestDynamicScreenComponent implements OnDestroy { + destroyed = false; + + ngOnDestroy(): void { + this.destroyed = true; + } +} + +@Component({ + selector: 'adf-cloud-test-host-screen', + template: `` +}) +class TestHostScreenComponent extends BaseScreenCloudComponent { + setInputsCalls: ComponentRef[] = []; + subscribeToOutputsCalls: ComponentRef[] = []; + + get dynamicComponentRef(): ComponentRef | undefined { + return this.componentRef; + } + + get dynamicComponentRefSignalValue(): ComponentRef | undefined { + return this.componentRefChanged(); + } + + protected override setInputsForDynamicComponent(componentRef: ComponentRef): void { + this.setInputsCalls.push(componentRef); + } + + protected subscribeToOutputs(componentRef: ComponentRef): void { + this.subscribeToOutputsCalls.push(componentRef); + } +} + +/** Same host component, but without the `#container` anchor in its template. */ +@Component({ + selector: 'adf-cloud-test-host-screen-without-container', + template: `
` +}) +class TestHostScreenWithoutContainerComponent extends TestHostScreenComponent {} + +describe('BaseScreenCloudComponent', () => { + const screenId = 'test-screen'; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestHostScreenComponent, TestHostScreenWithoutContainerComponent, TestDynamicScreenComponent], + providers: [provideScreen(screenId, TestDynamicScreenComponent)] + }); + }); + + describe('when a screenId is provided', () => { + let fixture: ComponentFixture; + let component: TestHostScreenComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostScreenComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('screenId', screenId); + fixture.detectChanges(); + }); + + it('should create the dynamic component and expose it through the signal', () => { + expect(component.dynamicComponentRef).toBeDefined(); + expect(component.dynamicComponentRefSignalValue).toBe(component.dynamicComponentRef); + expect(fixture.debugElement.query(By.css('.adf-cloud-test-dynamic-screen'))).toBeTruthy(); + }); + + it('should wire inputs and outputs once, passing the created component reference', () => { + expect(component.setInputsCalls).toEqual([component.dynamicComponentRef!]); + expect(component.subscribeToOutputsCalls).toEqual([component.dynamicComponentRef!]); + }); + + it('should destroy the dynamic component reference on destroy', () => { + const destroySpy = spyOn(component.dynamicComponentRef!, 'destroy').and.callThrough(); + + fixture.destroy(); + + expect(destroySpy).toHaveBeenCalledTimes(1); + }); + + it('should run the ngOnDestroy hook of the dynamic component on destroy', () => { + const dynamicComponentInstance = component.dynamicComponentRef?.instance; + expect(dynamicComponentInstance?.destroyed).toBeFalse(); + + fixture.destroy(); + + expect(dynamicComponentInstance?.destroyed).toBeTrue(); + }); + + it('should clear the dynamic component reference and the signal on destroy', () => { + fixture.destroy(); + + expect(component.dynamicComponentRef).toBeUndefined(); + expect(component.dynamicComponentRefSignalValue).toBeUndefined(); + }); + + it('should destroy the dynamic component reference only once when ngOnDestroy runs again', () => { + const destroySpy = spyOn(component.dynamicComponentRef!, 'destroy'); + + component.ngOnDestroy(); + component.ngOnDestroy(); + + expect(destroySpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('when no screenId is provided', () => { + let fixture: ComponentFixture; + let component: TestHostScreenComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should not create any dynamic component nor wire inputs and outputs', () => { + expect(component.dynamicComponentRef).toBeUndefined(); + expect(component.dynamicComponentRefSignalValue).toBeUndefined(); + expect(component.setInputsCalls).toEqual([]); + expect(component.subscribeToOutputsCalls).toEqual([]); + expect(fixture.debugElement.query(By.css('.adf-cloud-test-dynamic-screen'))).toBeNull(); + }); + + it('should not throw on destroy', () => { + expect(() => fixture.destroy()).not.toThrow(); + expect(component.dynamicComponentRef).toBeUndefined(); + }); + }); + + describe('when the container anchor is missing', () => { + let fixture: ComponentFixture; + let component: TestHostScreenWithoutContainerComponent; + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostScreenWithoutContainerComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('screenId', screenId); + }); + + it('should not throw and should not create any dynamic component', () => { + expect(() => fixture.detectChanges()).not.toThrow(); + + expect(component.container).toBeUndefined(); + expect(component.dynamicComponentRef).toBeUndefined(); + expect(component.dynamicComponentRefSignalValue).toBeUndefined(); + }); + + it('should not wire inputs and outputs when no dynamic component was created', () => { + fixture.detectChanges(); + + expect(component.setInputsCalls).toEqual([]); + expect(component.subscribeToOutputsCalls).toEqual([]); + }); + + it('should not resolve any component type', () => { + const resolveComponentTypeSpy = spyOn(TestBed.inject(ScreenRenderingService), 'resolveComponentType').and.callThrough(); + + fixture.detectChanges(); + + expect(resolveComponentTypeSpy).not.toHaveBeenCalled(); + }); + + it('should not throw on destroy', () => { + fixture.detectChanges(); + + expect(() => fixture.destroy()).not.toThrow(); + }); + }); +}); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts index b00f85c4da..7da72468dc 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/base-screen/base-screen-cloud.component.ts @@ -15,20 +15,20 @@ * limitations under the License. */ -import { Component, ComponentRef, inject, Input, OnInit, signal, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, ComponentRef, inject, Input, OnDestroy, OnInit, signal, ViewChild, ViewContainerRef } from '@angular/core'; import { ScreenRenderingService } from '../../../services/screen-rendering.service'; @Component({ template: '' }) -export abstract class BaseScreenCloudComponent implements OnInit { +export abstract class BaseScreenCloudComponent implements OnInit, OnDestroy { @Input() screenId: string = ''; @ViewChild('container', { read: ViewContainerRef, static: true }) - container: ViewContainerRef; + container: ViewContainerRef | undefined; - protected componentRef: ComponentRef; + protected componentRef: ComponentRef | undefined; private readonly _componentRefChanged = signal | undefined>(undefined); protected readonly componentRefChanged = this._componentRefChanged.asReadonly(); protected readonly screenRenderingService = inject(ScreenRenderingService); @@ -37,17 +37,27 @@ export abstract class BaseScreenCloudComponent imple this.createDynamicComponent(); } - private createDynamicComponent(): void { - if (this.screenId) { - const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId }); - this.componentRef = this.container.createComponent(componentType); - this._componentRefChanged.set(this.componentRef); - this.setInputsForDynamicComponent(); - this.subscribeToOutputs(); - } + ngOnDestroy(): void { + this.componentRef?.destroy(); + this.componentRef = undefined; + this._componentRefChanged.set(undefined); } - protected setInputsForDynamicComponent(): void {} + private createDynamicComponent(): void { + if (!this.screenId || !this.container) { + return; + } - protected abstract subscribeToOutputs(): void; + const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId }); + const componentRef: ComponentRef = this.container.createComponent(componentType); + + this.componentRef = componentRef; + this._componentRefChanged.set(componentRef); + this.setInputsForDynamicComponent(componentRef); + this.subscribeToOutputs(componentRef); + } + + protected setInputsForDynamicComponent(_componentRef: ComponentRef): void {} + + protected abstract subscribeToOutputs(componentRef: ComponentRef): void; } diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts index 937d1ad536..da37f04b9e 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.spec.ts @@ -16,6 +16,7 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, Input, OnDestroy, input, output } from '@angular/core'; import { StartProcessScreenCloudComponent } from './start-process-screen-cloud.component'; import { MockedTaskScreenCloudComponent } from '../../../../testing/start-process-screen-mock.component'; import { provideScreen } from '../../../services/provide-screen'; @@ -66,11 +67,11 @@ describe('StartProcessScreenCloudComponent', () => { it('should set appName', () => { const screenInstance: StartProcessScreenCloud = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)).componentInstance; - expect(screenInstance.appName()).toEqual(''); + expect(screenInstance.appName?.()).toEqual(''); const newValue = 'new-app-name'; fixture.componentRef.setInput('appName', newValue); fixture.detectChanges(); - expect(screenInstance.appName()).toEqual(newValue); + expect(screenInstance.appName?.()).toEqual(newValue); }); it('should set process definition id', () => { @@ -84,10 +85,125 @@ describe('StartProcessScreenCloudComponent', () => { it('should set resolvedValues', () => { const screenInstance: StartProcessScreenCloud = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)).componentInstance; - expect(screenInstance.resolvedValues()).toBeUndefined(); + expect(screenInstance.resolvedValues?.()).toBeUndefined(); const newValues = [new TaskVariableCloud({ id: 'new-id', name: 'new-name' })]; fixture.componentRef.setInput('resolvedValues', newValues); fixture.detectChanges(); - expect(screenInstance.resolvedValues()).toEqual(newValues); + expect(screenInstance.resolvedValues?.()).toEqual(newValues); + }); +}); + +@Component({ + selector: 'adf-cloud-destroy-tracking-screen', + template: `
screen
` +}) +class DestroyTrackingScreenComponent implements StartProcessScreenCloud, OnDestroy { + readonly appName = input(''); + processDefinitionId = input(''); + readonly resolvedValues = input(); + defaultStartProcessButtonsConfigurationChange = output(); + startProcessPayloadChanged = output(); + + destroyed = false; + + ngOnDestroy(): void { + this.destroyed = true; + } +} + +@Component({ + selector: 'adf-cloud-test-start-process-wrapper', + template: ` + @if (showScreen) { + + } + `, + imports: [StartProcessScreenCloudComponent] +}) +class TestStartProcessWrapperComponent { + @Input() screenId = ''; + showScreen = true; +} + +describe('StartProcessScreenCloudComponent - destroy', () => { + let fixture: ComponentFixture; + let component: TestStartProcessWrapperComponent; + const screenId = 'screen-1234-5678-121212-123456'; + + const getScreenInstance = (): DestroyTrackingScreenComponent => + fixture.debugElement.query(By.directive(DestroyTrackingScreenComponent)).componentInstance; + + const destroyScreen = () => { + component.showScreen = false; + fixture.detectChanges(); + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestStartProcessWrapperComponent], + providers: [provideScreen(screenId, DestroyTrackingScreenComponent)] + }); + + fixture = TestBed.createComponent(TestStartProcessWrapperComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('screenId', screenId); + fixture.detectChanges(); + }); + + it('should destroy the screen component when the host is destroyed', () => { + const screenInstance = getScreenInstance(); + expect(screenInstance.destroyed).toBeFalse(); + + destroyScreen(); + + expect(screenInstance.destroyed).toBeTrue(); + }); + + it('should remove the screen component from the DOM when the host is destroyed', () => { + expect(fixture.debugElement.query(By.css('.adf-cloud-destroy-tracking-screen'))).toBeTruthy(); + + destroyScreen(); + + expect(fixture.debugElement.query(By.css('.adf-cloud-destroy-tracking-screen'))).toBeNull(); + }); + + it('should create a new screen component instance when the host is re-created', () => { + const firstInstance = getScreenInstance(); + + destroyScreen(); + component.showScreen = true; + fixture.detectChanges(); + + const secondInstance = getScreenInstance(); + expect(secondInstance).not.toBe(firstInstance); + expect(secondInstance.destroyed).toBeFalse(); + expect(secondInstance.processDefinitionId()).toBe('definition-id'); + }); +}); + +describe('StartProcessScreenCloudComponent - without screenId', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [StartProcessScreenCloudComponent] + }); + fixture = TestBed.createComponent(StartProcessScreenCloudComponent); + }); + + it('should not create any screen component and should not throw', () => { + expect(() => fixture.detectChanges()).not.toThrow(); + expect(fixture.debugElement.query(By.directive(DestroyTrackingScreenComponent))).toBeNull(); + }); + + it('should not throw when inputs change or on destroy', () => { + fixture.detectChanges(); + + expect(() => { + fixture.componentRef.setInput('appName', 'new-app-name'); + fixture.componentRef.setInput('resolvedValues', [new TaskVariableCloud({ id: 'id', name: 'name' })]); + fixture.detectChanges(); + }).not.toThrow(); + expect(() => fixture.destroy()).not.toThrow(); }); }); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts index 0f9c94dc81..ce069b6cdb 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, effect, input, output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ComponentRef, effect, input, output, signal } from '@angular/core'; import { BaseScreenCloudComponent } from '../base-screen/base-screen-cloud.component'; import { MatCardModule } from '@angular/material/card'; import { CommonModule } from '@angular/common'; @@ -43,7 +43,7 @@ export class StartProcessScreenCloudComponent extends BaseScreenCloudComponent { const componentRef = this.componentRefChanged(); - if (componentRef.instance && 'appName' in componentRef.instance) { + if (componentRef?.instance && 'appName' in componentRef.instance) { componentRef.setInput('appName', this.appName()); } }); @@ -56,9 +56,9 @@ export class StartProcessScreenCloudComponent extends BaseScreenCloudComponent this.screenStartProcessPayloadChange.emit(payload)); - this.componentRef.instance.defaultStartProcessButtonsConfigurationChange.subscribe((config) => { + protected subscribeToOutputs(componentRef: ComponentRef): void { + componentRef.instance.startProcessPayloadChanged.subscribe((payload) => this.screenStartProcessPayloadChange.emit(payload)); + componentRef.instance.defaultStartProcessButtonsConfigurationChange.subscribe((config) => { this.showStartProcessButtons.set(config.show); this.disableStartProcessButton.emit(config.disable); }); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts index 3bf74e8816..e271a8a6e6 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.spec.ts @@ -16,7 +16,7 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ScreenRenderingService } from '../../../services/screen-rendering.service'; import { TaskScreenCloudComponent } from './screen-cloud.component'; @@ -32,18 +32,22 @@ import { TaskScreenCloudComponent } from './screen-cloud.component'; ` }) -class TestComponent { +class TestComponent implements OnDestroy { @Input() taskId = ''; @Input() screenId = ''; @Input() rootProcessInstanceId = ''; @Output() taskCompleted = new EventEmitter(); - displayMode: string; + displayMode: string | undefined; + destroyed = false; onComplete() { this.taskCompleted.emit(); } switchToDisplayMode(newDisplayMode?: string) { this.displayMode = newDisplayMode; } + ngOnDestroy(): void { + this.destroyed = true; + } } @Component({ @@ -61,7 +65,7 @@ class TestComponent { }) class TestWrapperComponent { @Input() screenId = ''; - @ViewChild('adfCloudTaskScreen') adfCloudTaskScreen: TaskScreenCloudComponent; + @ViewChild('adfCloudTaskScreen') adfCloudTaskScreen: TaskScreenCloudComponent | undefined; onTaskCompleted() {} switchToDisplayMode(newDisplayMode?: string): void { if (this.adfCloudTaskScreen) { @@ -118,6 +122,118 @@ describe('TaskScreenCloudComponent', () => { component.switchToDisplayMode(); fixture.detectChanges(); - expect(component.adfCloudTaskScreen.switchToDisplayMode).toHaveBeenCalled(); + expect(component.adfCloudTaskScreen?.switchToDisplayMode).toHaveBeenCalled(); + }); +}); + +@Component({ + selector: 'adf-cloud-test-conditional-component', + template: ` + @if (showTaskScreen) { + + } + `, + imports: [TaskScreenCloudComponent] +}) +class TestConditionalWrapperComponent { + showTaskScreen = true; + onTaskCompleted() {} +} + +describe('TaskScreenCloudComponent - destroy', () => { + let fixture: ComponentFixture; + let component: TestConditionalWrapperComponent; + + const getDynamicComponentInstance = (): TestComponent => fixture.debugElement.query(By.directive(TestComponent)).componentInstance; + + const getTaskScreen = (): TaskScreenCloudComponent => fixture.debugElement.query(By.directive(TaskScreenCloudComponent)).componentInstance; + + const destroyTaskScreen = () => { + component.showTaskScreen = false; + fixture.detectChanges(); + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TaskScreenCloudComponent, TestComponent, TestConditionalWrapperComponent] + }); + TestBed.inject(ScreenRenderingService).register({ ['test']: () => TestComponent }); + + fixture = TestBed.createComponent(TestConditionalWrapperComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should destroy the dynamic component when the task screen is destroyed', () => { + const dynamicComponentInstance = getDynamicComponentInstance(); + expect(dynamicComponentInstance.destroyed).toBeFalse(); + + destroyTaskScreen(); + + expect(dynamicComponentInstance.destroyed).toBeTrue(); + }); + + it('should remove the dynamic component from the DOM when the task screen is destroyed', () => { + expect(fixture.debugElement.query(By.css('.adf-cloud-test-container'))).toBeTruthy(); + + destroyTaskScreen(); + + expect(fixture.debugElement.query(By.css('.adf-cloud-test-container'))).toBeNull(); + }); + + it('should not emit outputs of the dynamic component after the task screen is destroyed', () => { + const onTaskCompletedSpy = spyOn(component, 'onTaskCompleted'); + const dynamicComponentInstance = getDynamicComponentInstance(); + + destroyTaskScreen(); + dynamicComponentInstance.taskCompleted.emit(); + + expect(onTaskCompletedSpy).not.toHaveBeenCalled(); + }); + + it('should not call the dynamic component when switching display mode after destroy', () => { + const taskScreen = getTaskScreen(); + const switchToDisplayModeSpy = spyOn(getDynamicComponentInstance(), 'switchToDisplayMode'); + + destroyTaskScreen(); + + expect(() => taskScreen.switchToDisplayMode('mode')).not.toThrow(); + expect(switchToDisplayModeSpy).not.toHaveBeenCalled(); + }); + + it('should create a new dynamic component instance when the task screen is re-created', () => { + const firstInstance = getDynamicComponentInstance(); + + destroyTaskScreen(); + component.showTaskScreen = true; + fixture.detectChanges(); + + const secondInstance = getDynamicComponentInstance(); + expect(secondInstance).not.toBe(firstInstance); + expect(secondInstance.destroyed).toBeFalse(); + expect(secondInstance.taskId).toBe('1'); + }); +}); + +describe('TaskScreenCloudComponent - without screenId', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TaskScreenCloudComponent] + }); + fixture = TestBed.createComponent(TaskScreenCloudComponent); + }); + + it('should not create any dynamic component and should not throw', () => { + expect(() => fixture.detectChanges()).not.toThrow(); + expect(fixture.debugElement.query(By.directive(TestComponent))).toBeNull(); + }); + + it('should not throw when switching display mode or destroying', () => { + fixture.detectChanges(); + + expect(() => fixture.componentInstance.switchToDisplayMode('mode')).not.toThrow(); + expect(() => fixture.destroy()).not.toThrow(); }); }); diff --git a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts index 14ad0b5893..5069d34735 100644 --- a/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/screen/components/screen-cloud/user-task-screen/screen-cloud.component.ts @@ -16,7 +16,7 @@ */ import { CommonModule } from '@angular/common'; -import { Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core'; +import { Component, ComponentRef, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { UserTaskCustomUi } from './screen-cloud.model'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -102,65 +102,65 @@ export class TaskScreenCloudComponent extends BaseScreenCloudComponent): void { + if (this.taskId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'taskId')) { + componentRef.setInput('taskId', this.taskId); } - if (this.appName && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'appName')) { - this.componentRef.setInput('appName', this.appName); + if (this.appName && Object.prototype.hasOwnProperty.call(componentRef.instance, 'appName')) { + componentRef.setInput('appName', this.appName); } - if (this.screenId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'screenId')) { - this.componentRef.setInput('screenId', this.screenId); + if (this.screenId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'screenId')) { + componentRef.setInput('screenId', this.screenId); } - if (this.processInstanceId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'processInstanceId')) { - this.componentRef.setInput('processInstanceId', this.processInstanceId); + if (this.processInstanceId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'processInstanceId')) { + componentRef.setInput('processInstanceId', this.processInstanceId); } - if (this.taskName && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'taskName')) { - this.componentRef.setInput('taskName', this.taskName); + if (this.taskName && Object.prototype.hasOwnProperty.call(componentRef.instance, 'taskName')) { + componentRef.setInput('taskName', this.taskName); } - if (this.canClaimTask && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'canClaimTask')) { - this.componentRef.setInput('canClaimTask', this.canClaimTask); + if (this.canClaimTask && Object.prototype.hasOwnProperty.call(componentRef.instance, 'canClaimTask')) { + componentRef.setInput('canClaimTask', this.canClaimTask); } - if (this.canUnclaimTask && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'canUnclaimTask')) { - this.componentRef.setInput('canUnclaimTask', this.canUnclaimTask); + if (this.canUnclaimTask && Object.prototype.hasOwnProperty.call(componentRef.instance, 'canUnclaimTask')) { + componentRef.setInput('canUnclaimTask', this.canUnclaimTask); } - if (this.showCancelButton && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'showCancelButton')) { - this.componentRef.setInput('showCancelButton', this.showCancelButton); + if (this.showCancelButton && Object.prototype.hasOwnProperty.call(componentRef.instance, 'showCancelButton')) { + componentRef.setInput('showCancelButton', this.showCancelButton); } - if (this.rootProcessInstanceId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'rootProcessInstanceId')) { - this.componentRef.setInput('rootProcessInstanceId', this.rootProcessInstanceId); + if (this.rootProcessInstanceId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'rootProcessInstanceId')) { + componentRef.setInput('rootProcessInstanceId', this.rootProcessInstanceId); } - if (this.showNextTaskCheckbox && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'showNextTaskCheckbox')) { - this.componentRef.setInput('showNextTaskCheckbox', this.showNextTaskCheckbox); + if (this.showNextTaskCheckbox && Object.prototype.hasOwnProperty.call(componentRef.instance, 'showNextTaskCheckbox')) { + componentRef.setInput('showNextTaskCheckbox', this.showNextTaskCheckbox); } - if (this.isNextTaskCheckboxChecked && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'isNextTaskCheckboxChecked')) { - this.componentRef.setInput('isNextTaskCheckboxChecked', this.isNextTaskCheckboxChecked); + if (this.isNextTaskCheckboxChecked && Object.prototype.hasOwnProperty.call(componentRef.instance, 'isNextTaskCheckboxChecked')) { + componentRef.setInput('isNextTaskCheckboxChecked', this.isNextTaskCheckboxChecked); } } - protected override subscribeToOutputs(): void { - if (this.componentRef.instance?.taskSaved) { - this.componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit()); + protected override subscribeToOutputs(componentRef: ComponentRef): void { + if (componentRef.instance?.taskSaved) { + componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit()); } - if (this.componentRef.instance?.taskCompleted) { - this.componentRef.instance.taskCompleted + if (componentRef.instance?.taskCompleted) { + componentRef.instance.taskCompleted .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((openNextTask) => this.taskCompleted.emit(openNextTask)); } - if (this.componentRef.instance?.error) { - this.componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data)); + if (componentRef.instance?.error) { + componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data)); } - if (this.componentRef.instance?.claimTask) { - this.componentRef.instance.claimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.claimTask.emit(data)); + if (componentRef.instance?.claimTask) { + componentRef.instance.claimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.claimTask.emit(data)); } - if (this.componentRef.instance?.unclaimTask) { - this.componentRef.instance.unclaimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.unclaimTask.emit(data)); + if (componentRef.instance?.unclaimTask) { + componentRef.instance.unclaimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.unclaimTask.emit(data)); } - if (this.componentRef.instance?.cancelTask) { - this.componentRef.instance.cancelTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.cancelTask.emit(data)); + if (componentRef.instance?.cancelTask) { + componentRef.instance.cancelTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.cancelTask.emit(data)); } - if (this.componentRef.instance?.nextTaskCheckboxCheckedChanged) { - this.componentRef.instance.nextTaskCheckboxCheckedChanged + if (componentRef.instance?.nextTaskCheckboxCheckedChanged) { + componentRef.instance.nextTaskCheckboxCheckedChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data) => this.nextTaskCheckboxCheckedChanged.emit(data)); }