ACS-11154 Viewer stabilisation, lint fixes (#11660)

* refactor: clean up code in SuperagentHttpClient and upload.spec

- Removed unnecessary eslint-disable comment in SuperagentHttpClient for improved code clarity.
- Refactored promise handling in upload.spec to utilize an array for better management of multiple promises during file upload error handling.

* chore: update cspell and ESLint configurations

- Added "webscript" to the cspell dictionary for improved spell checking.
- Updated ESLint configuration to disable the "@typescript-eslint/no-explicit-any" rule, allowing more flexibility in type definitions.

* fix: enhance ImgViewerComponent to handle cleanup and prevent errors after destruction

- Added a `destroyed` flag to manage component lifecycle and prevent operations on a destroyed instance.
- Implemented `afterEach` hooks in tests to ensure proper fixture cleanup.
- Updated key event handlers and methods to check for the `destroyed` state before executing actions, improving stability and preventing errors.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: add aria-hidden attribute to notification history button for accessibility

- Updated the notification history button to include the `aria-hidden` attribute, improving accessibility for screen readers and enhancing user experience.

* fix: improve key event handling in ImgViewerComponent

- Updated key event handlers to check for the presence of the cropper before executing actions, enhancing stability and preventing errors when the component is destroyed.
- Removed redundant checks from individual arrow key handlers, streamlining the code.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denys Vuika
2026-02-17 12:16:49 +00:00
committed by GitHub
co-authored by Cursor
parent 84affe9f37
commit 1f49af4c8f
7 changed files with 81 additions and 19 deletions
@@ -8,7 +8,7 @@
id="adf-notification-history-open-button"
(menuOpened)="onMenuOpened()"
>
<mat-icon matBadge="&#8288;" [matBadgeHidden]="!notifications.length" class="adf-notification-history-menu_button-icon" matBadgeColor="accent" matBadgeSize="small" adf-icon="notifications" />
<mat-icon aria-hidden="false" matBadge="&#8288;" [matBadgeHidden]="!notifications.length" class="adf-notification-history-menu_button-icon" matBadgeColor="accent" matBadgeSize="small" adf-icon="notifications" />
</button>
<mat-menu #menu="matMenu"
@@ -64,6 +64,10 @@ describe('Test Img viewer component ', () => {
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
describe('default value', () => {
it('should use default zoom if is not present a custom zoom in the app.config', () => {
fixture.detectChanges();
@@ -103,6 +107,10 @@ describe('Test Img viewer component ', () => {
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should display current scale as percent string', () => {
component.scale = 0.5;
expect(component.currentScaleText).toBe('50%');
@@ -127,6 +135,10 @@ describe('Test Img viewer component ', () => {
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should thrown an error if no url or blob are passed', () => {
const change = new SimpleChange(null, null, true);
expect(() => {
@@ -181,6 +193,10 @@ describe('Test Img viewer component ', () => {
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should update scales on zoom in', fakeAsync(() => {
spyOn(component, 'zoomIn').and.callThrough();
spyOn(component.cropper, 'zoom');
@@ -377,6 +393,10 @@ describe('Test Img viewer component ', () => {
component = fixture.componentInstance;
});
afterEach(() => {
fixture.destroy();
});
it('should conditionally display rotate and crop buttons based on allowedEditActions', () => {
component.readOnly = false;
component.allowedEditActions = { rotate: true, crop: true };
@@ -401,7 +421,14 @@ describe('Test Img viewer component ', () => {
fixture = TestBed.createComponent(ImgViewerComponent);
testingUtils = new UnitTestingUtils(fixture.debugElement);
component = fixture.componentInstance;
component.urlFile =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
fixture.detectChanges();
component.ngAfterViewInit();
});
afterEach(() => {
fixture.destroy();
});
it('should rotate the image when r key is pressed', () => {
@@ -428,101 +455,119 @@ describe('Test Img viewer component ', () => {
expect(component.zoomIn).toHaveBeenCalled();
});
it('should move the cropper when arrow keys are pressed', () => {
it('should move the cropper when arrow keys are pressed', fakeAsync(() => {
spyOn(component.cropper, 'move');
dispatchKeyboardEvent('ArrowLeft');
fixture.detectChanges();
tick();
expect(component.cropper.move).toHaveBeenCalledWith(-3, 0);
dispatchKeyboardEvent('ArrowRight');
fixture.detectChanges();
tick();
expect(component.cropper.move).toHaveBeenCalledWith(3, 0);
dispatchKeyboardEvent('ArrowUp');
fixture.detectChanges();
tick();
expect(component.cropper.move).toHaveBeenCalledWith(0, -3);
dispatchKeyboardEvent('ArrowDown');
fixture.detectChanges();
tick();
expect(component.cropper.move).toHaveBeenCalledWith(0, 3);
});
}));
it('should increase crop box area when arrow keys with shift are pressed', () => {
it('should increase crop box area when arrow keys with shift are pressed', fakeAsync(() => {
component.cropImage();
spyOn(component.cropper, 'setCropBoxData');
let expectedCropBoxData = getExpectedCropBoxData(component.cropper, -3, 3, 0, 0);
dispatchKeyboardEvent('ArrowLeft', true);
fixture.detectChanges();
tick();
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 3, 0, 0);
dispatchKeyboardEvent('ArrowRight', true);
fixture.detectChanges();
tick();
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 0, -3, 3);
dispatchKeyboardEvent('ArrowUp', true);
fixture.detectChanges();
tick();
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 0, 0, 3);
dispatchKeyboardEvent('ArrowDown', true);
fixture.detectChanges();
tick();
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
});
}));
it('should decrease crop box area when arrow keys with alt are pressed', () => {
it('should decrease crop box area when arrow keys with alt are pressed', fakeAsync(() => {
component.cropImage();
spyOn(component.cropper, 'setCropBoxData');
let expectedCropBoxData = getExpectedCropBoxData(component.cropper, 3, -3, 0, 0);
dispatchKeyboardEvent('ArrowLeft', false, true);
fixture.detectChanges();
tick();
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, -3, 0, 0);
dispatchKeyboardEvent('ArrowRight', false, true);
fixture.detectChanges();
tick();
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 0, 3, -3);
dispatchKeyboardEvent('ArrowUp', false, true);
fixture.detectChanges();
tick();
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
expectedCropBoxData = getExpectedCropBoxData(component.cropper, 0, 0, 0, -3);
dispatchKeyboardEvent('ArrowDown', false, true);
fixture.detectChanges();
tick();
expect(component.cropper.setCropBoxData).toHaveBeenCalledWith(expectedCropBoxData);
});
}));
it('should prevent default for all arrow keys events', fakeAsync(() => {
spyOn(component.cropper, 'move');
it('should prevent default for all arrow keys events', () => {
const leftEvent = dispatchKeyboardEvent('ArrowLeft');
fixture.detectChanges();
tick();
expect(leftEvent.preventDefault).toHaveBeenCalled();
const rightEvent = dispatchKeyboardEvent('ArrowRight');
fixture.detectChanges();
tick();
expect(rightEvent.preventDefault).toHaveBeenCalled();
const upEvent = dispatchKeyboardEvent('ArrowUp');
fixture.detectChanges();
tick();
expect(upEvent.preventDefault).toHaveBeenCalled();
const downEvent = dispatchKeyboardEvent('ArrowDown');
fixture.detectChanges();
tick();
expect(downEvent.preventDefault).toHaveBeenCalled();
});
}));
});
});
@@ -87,6 +87,9 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
@HostListener('document:keyup', ['$event'])
onKeyDown(event: KeyboardEvent) {
if (this.destroyed || !this.cropper) {
return;
}
switch (event.key) {
case 'ArrowLeft': {
this.handleArrowLeftKey(event);
@@ -130,6 +133,7 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
scale: number = 1.0;
cropper: Cropper;
isEditing: boolean = false;
private destroyed: boolean = false;
get currentScaleText(): string {
return Math.round(this.scale * 100) + '%';
@@ -178,7 +182,11 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
}
ngOnDestroy() {
this.cropper.destroy();
this.destroyed = true;
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
}
initializeScaling() {
@@ -189,11 +197,17 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
}
zoomIn() {
if (this.destroyed || !this.cropper) {
return;
}
this.cropper.zoom(0.2);
this.scale = +(this.scale + 0.2).toFixed(1);
}
zoomOut() {
if (this.destroyed || !this.cropper) {
return;
}
if (this.scale > 0.2) {
this.cropper.zoom(-0.2);
this.scale = +(this.scale - 0.2).toFixed(1);
@@ -201,6 +215,9 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
}
rotateImage() {
if (this.destroyed || !this.cropper) {
return;
}
this.isEditing = true;
this.cropper.rotate(-90);
}