mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-50665 Handling dynamic component destroy (#12183)
* AAE-50665 Handling dynamic component destroy * AAE-50665 Code improvements
This commit is contained in:
+198
@@ -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: `<div class="adf-cloud-test-dynamic-screen">dynamic screen</div>`
|
||||
})
|
||||
class TestDynamicScreenComponent implements OnDestroy {
|
||||
destroyed = false;
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'adf-cloud-test-host-screen',
|
||||
template: `<ng-container #container />`
|
||||
})
|
||||
class TestHostScreenComponent extends BaseScreenCloudComponent<TestDynamicScreenComponent> {
|
||||
setInputsCalls: ComponentRef<TestDynamicScreenComponent>[] = [];
|
||||
subscribeToOutputsCalls: ComponentRef<TestDynamicScreenComponent>[] = [];
|
||||
|
||||
get dynamicComponentRef(): ComponentRef<TestDynamicScreenComponent> | undefined {
|
||||
return this.componentRef;
|
||||
}
|
||||
|
||||
get dynamicComponentRefSignalValue(): ComponentRef<TestDynamicScreenComponent> | undefined {
|
||||
return this.componentRefChanged();
|
||||
}
|
||||
|
||||
protected override setInputsForDynamicComponent(componentRef: ComponentRef<TestDynamicScreenComponent>): void {
|
||||
this.setInputsCalls.push(componentRef);
|
||||
}
|
||||
|
||||
protected subscribeToOutputs(componentRef: ComponentRef<TestDynamicScreenComponent>): 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: `<div class="adf-cloud-no-container"></div>`
|
||||
})
|
||||
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<TestHostScreenComponent>;
|
||||
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<TestHostScreenComponent>;
|
||||
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<TestHostScreenWithoutContainerComponent>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+24
-14
@@ -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<TScreenComponent = unknown> implements OnInit {
|
||||
export abstract class BaseScreenCloudComponent<TScreenComponent = unknown> implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
screenId: string = '';
|
||||
|
||||
@ViewChild('container', { read: ViewContainerRef, static: true })
|
||||
container: ViewContainerRef;
|
||||
container: ViewContainerRef | undefined;
|
||||
|
||||
protected componentRef: ComponentRef<TScreenComponent>;
|
||||
protected componentRef: ComponentRef<TScreenComponent> | undefined;
|
||||
private readonly _componentRefChanged = signal<ComponentRef<TScreenComponent> | undefined>(undefined);
|
||||
protected readonly componentRefChanged = this._componentRefChanged.asReadonly();
|
||||
protected readonly screenRenderingService = inject(ScreenRenderingService);
|
||||
@@ -37,17 +37,27 @@ export abstract class BaseScreenCloudComponent<TScreenComponent = unknown> 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<TScreenComponent> = this.container.createComponent(componentType);
|
||||
|
||||
this.componentRef = componentRef;
|
||||
this._componentRefChanged.set(componentRef);
|
||||
this.setInputsForDynamicComponent(componentRef);
|
||||
this.subscribeToOutputs(componentRef);
|
||||
}
|
||||
|
||||
protected setInputsForDynamicComponent(_componentRef: ComponentRef<TScreenComponent>): void {}
|
||||
|
||||
protected abstract subscribeToOutputs(componentRef: ComponentRef<TScreenComponent>): void;
|
||||
}
|
||||
|
||||
+120
-4
@@ -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: `<div class="adf-cloud-destroy-tracking-screen">screen</div>`
|
||||
})
|
||||
class DestroyTrackingScreenComponent implements StartProcessScreenCloud, OnDestroy {
|
||||
readonly appName = input('');
|
||||
processDefinitionId = input('');
|
||||
readonly resolvedValues = input<TaskVariableCloud[] | undefined>();
|
||||
defaultStartProcessButtonsConfigurationChange = output<StartProcessScreenDefaultButtons>();
|
||||
startProcessPayloadChanged = output<unknown>();
|
||||
|
||||
destroyed = false;
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'adf-cloud-test-start-process-wrapper',
|
||||
template: `
|
||||
@if (showScreen) {
|
||||
<adf-cloud-start-process-screen-cloud [screenId]="screenId" [processDefinitionId]="'definition-id'" />
|
||||
}
|
||||
`,
|
||||
imports: [StartProcessScreenCloudComponent]
|
||||
})
|
||||
class TestStartProcessWrapperComponent {
|
||||
@Input() screenId = '';
|
||||
showScreen = true;
|
||||
}
|
||||
|
||||
describe('StartProcessScreenCloudComponent - destroy', () => {
|
||||
let fixture: ComponentFixture<TestStartProcessWrapperComponent>;
|
||||
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<StartProcessScreenCloudComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
+5
-5
@@ -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<S
|
||||
super();
|
||||
effect(() => {
|
||||
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<S
|
||||
});
|
||||
}
|
||||
|
||||
protected subscribeToOutputs(): void {
|
||||
this.componentRef.instance.startProcessPayloadChanged.subscribe((payload) => this.screenStartProcessPayloadChange.emit(payload));
|
||||
this.componentRef.instance.defaultStartProcessButtonsConfigurationChange.subscribe((config) => {
|
||||
protected subscribeToOutputs(componentRef: ComponentRef<StartProcessScreenCloud>): 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);
|
||||
});
|
||||
|
||||
+121
-5
@@ -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';
|
||||
</div>
|
||||
`
|
||||
})
|
||||
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) {
|
||||
<adf-cloud-task-screen [taskId]="'1'" [appName]="'app-name-test'" [screenId]="'test'" (taskCompleted)="onTaskCompleted()" />
|
||||
}
|
||||
`,
|
||||
imports: [TaskScreenCloudComponent]
|
||||
})
|
||||
class TestConditionalWrapperComponent {
|
||||
showTaskScreen = true;
|
||||
onTaskCompleted() {}
|
||||
}
|
||||
|
||||
describe('TaskScreenCloudComponent - destroy', () => {
|
||||
let fixture: ComponentFixture<TestConditionalWrapperComponent>;
|
||||
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<TaskScreenCloudComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
+39
-39
@@ -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<UserTaskC
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
protected override setInputsForDynamicComponent(): void {
|
||||
if (this.taskId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'taskId')) {
|
||||
this.componentRef.setInput('taskId', this.taskId);
|
||||
protected override setInputsForDynamicComponent(componentRef: ComponentRef<UserTaskCustomUi>): 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<UserTaskCustomUi>): 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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user