AAE-44809 Fix form multi-file attachment - document viewer not updated on file selection (#11980)

This commit is contained in:
David Olson
2026-06-12 16:53:51 -05:00
committed by GitHub
parent 9d353bf271
commit 5dd709ed58
4 changed files with 145 additions and 4 deletions
@@ -0,0 +1,87 @@
/*!
* @license
* Copyright © 2005-2025 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 { TestBed } from '@angular/core/testing';
import { DomSanitizer } from '@angular/platform-browser';
import { UrlService } from './url.service';
describe('UrlService', () => {
const fakeObjectUrl = 'blob:http://localhost/fake-object-url';
const fakeSafeUrl = { __safe: 'safe-url' } as unknown as string;
let service: UrlService;
let sanitizer: DomSanitizer;
let createObjectURLSpy: jasmine.Spy;
beforeEach(() => {
const mockSanitizer = {
bypassSecurityTrustUrl: jasmine.createSpy('bypassSecurityTrustUrl').and.returnValue(fakeSafeUrl)
};
TestBed.configureTestingModule({
providers: [{ provide: DomSanitizer, useValue: mockSanitizer }]
});
service = TestBed.inject(UrlService);
sanitizer = TestBed.inject(DomSanitizer);
createObjectURLSpy = spyOn(window.URL, 'createObjectURL').and.returnValue(fakeObjectUrl);
});
describe('createObjectUrl', () => {
it('should delegate to window.URL.createObjectURL and return the raw url', () => {
const blob = new Blob(['test'], { type: 'text/plain' });
const result = service.createObjectUrl(blob);
expect(createObjectURLSpy).toHaveBeenCalledOnceWith(blob);
expect(result).toBe(fakeObjectUrl);
});
it('should not invoke the sanitizer', () => {
service.createObjectUrl(new Blob(['test']));
expect(sanitizer.bypassSecurityTrustUrl).not.toHaveBeenCalled();
});
});
describe('trustUrl', () => {
it('should wrap the url with bypassSecurityTrustUrl', () => {
const result = service.trustUrl(fakeObjectUrl);
expect(sanitizer.bypassSecurityTrustUrl).toHaveBeenCalledOnceWith(fakeObjectUrl);
expect(result).toBe(fakeSafeUrl);
});
it('should not create an object url', () => {
service.trustUrl(fakeObjectUrl);
expect(createObjectURLSpy).not.toHaveBeenCalled();
});
});
describe('createTrustedUrl', () => {
it('should create an object url from the blob and pass it to the sanitizer', () => {
const blob = new Blob(['test'], { type: 'text/plain' });
const result = service.createTrustedUrl(blob);
expect(createObjectURLSpy).toHaveBeenCalledOnceWith(blob);
expect(sanitizer.bypassSecurityTrustUrl).toHaveBeenCalledOnceWith(fakeObjectUrl);
expect(result).toBe(fakeSafeUrl);
});
});
});
@@ -32,7 +32,28 @@ export class UrlService {
* @returns URL string
*/
createTrustedUrl(blob: Blob): string {
const url = window.URL.createObjectURL(blob);
return this.trustUrl(this.createObjectUrl(blob));
}
/**
* Creates a raw object URL from the Blob without Angular sanitization. Use this for
* non-Angular consumers (third-party DOM APIs, libraries) that need a real URL string.
*
* @param blob Data to wrap into object URL
* @returns Raw object URL string
*/
createObjectUrl(blob: Blob): string {
return window.URL.createObjectURL(blob);
}
/**
* Wraps a URL string with Angular's bypassSecurityTrustUrl so it can be used in template bindings.
* WARNING: calling this method with untrusted user data exposes your application to XSS security risks!
*
* @param url URL to mark as safe
* @returns SafeUrl wrapper cast to string for template binding
*/
trustUrl(url: string): string {
return this.sanitizer.bypassSecurityTrustUrl(url) as string;
}
}
@@ -173,12 +173,41 @@ describe('Test Img viewer component ', () => {
it('should not thrown an error if blob is passed ', () => {
const blob = createFakeBlob();
spyOn(urlService, 'createTrustedUrl').and.returnValue('fake-blob-url');
spyOn(urlService, 'createObjectUrl').and.returnValue('fake-blob-url');
spyOn(urlService, 'trustUrl').and.returnValue('safe-fake-blob-url');
const change = new SimpleChange(null, blob, true);
expect(() => {
component.ngOnChanges({ blobFile: change });
}).not.toThrow(new Error('Attribute urlFile or blobFile is required'));
expect(component.urlFile).toEqual('fake-blob-url');
expect(component.urlFile).toEqual('safe-fake-blob-url');
});
it('should call replace on cropper with the raw blob url when blobFile changes after init', () => {
component.urlFile = 'fake-url';
fixture.detectChanges();
spyOn(component.cropper, 'replace').and.stub();
spyOn(urlService, 'createObjectUrl').and.returnValue('fake-blob-url-2');
spyOn(urlService, 'trustUrl').and.returnValue('safe-fake-blob-url-2');
const blobFile = new SimpleChange(createFakeBlob(), createFakeBlob(), false);
component.ngOnChanges({ blobFile });
expect(component.cropper.replace).toHaveBeenCalledWith('fake-blob-url-2');
expect(component.urlFile).toEqual('safe-fake-blob-url-2');
});
it('should not call replace on cropper on the first blobFile change', () => {
component.urlFile = 'fake-url';
fixture.detectChanges();
spyOn(component.cropper, 'replace').and.stub();
spyOn(urlService, 'createObjectUrl').and.returnValue('fake-blob-url');
spyOn(urlService, 'trustUrl').and.returnValue('safe-fake-blob-url');
const blobFile = new SimpleChange(null, createFakeBlob(), true);
component.ngOnChanges({ blobFile });
expect(component.cropper.replace).not.toHaveBeenCalled();
expect(component.urlFile).toEqual('safe-fake-blob-url');
});
});
@@ -150,7 +150,11 @@ export class ImgViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges) {
const blobFile = changes['blobFile'];
if (blobFile?.currentValue) {
this.urlFile = this.urlService.createTrustedUrl(this.blobFile);
const rawUrl = this.urlService.createObjectUrl(this.blobFile);
if (!blobFile.firstChange && this.cropper) {
this.cropper.replace(rawUrl);
}
this.urlFile = this.urlService.trustUrl(rawUrl);
return;
}