diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 1421b706..46f6d426 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -5,5 +5,5 @@ "types": ["cypress", "node"], "resolveJsonModule": true }, - "include": ["**/*.ts"] + "include": ["**/*.ts", "../cypress.config.ts"] } diff --git a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts new file mode 100644 index 00000000..6dd2bb30 --- /dev/null +++ b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.spec.ts @@ -0,0 +1,108 @@ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick +} from '@angular/core/testing'; +import { CpsTooltipDirective } from './cps-tooltip.directive'; +import { Component } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +@Component({ + template: `
`, + imports: [CpsTooltipDirective] +}) +class MaliciousTooltipComponent {} + +@Component({ + template: `
`, + imports: [CpsTooltipDirective] +}) +class LegitTooltipComponent {} + +describe('CpsTooltipDirective', () => { + let legitComponent: LegitTooltipComponent; + let legitComponentFixture: ComponentFixture; + + let maliciousComponent: MaliciousTooltipComponent; + let maliciousComponentFixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [] + }).compileComponents(); + }); + + beforeEach(() => { + legitComponentFixture = TestBed.createComponent(LegitTooltipComponent); + legitComponent = legitComponentFixture.componentInstance; + legitComponentFixture.detectChanges(); + + maliciousComponentFixture = TestBed.createComponent( + MaliciousTooltipComponent + ); + maliciousComponent = maliciousComponentFixture.componentInstance; + maliciousComponentFixture.detectChanges(); + }); + + it('should create the component', () => { + expect(maliciousComponent).toBeTruthy(); + expect(legitComponent).toBeTruthy(); + }); + + it('should sanitize the malicious tooltip content', fakeAsync(() => { + const consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + const divElement = maliciousComponentFixture.debugElement.query( + By.css('div') + ); + divElement.triggerEventHandler('mouseenter', null); + maliciousComponentFixture.detectChanges(); + + tick(300); + + const tooltipElement: HTMLElement | null = + document.body.querySelector('.cps-tooltip'); + + expect(tooltipElement).toBeTruthy(); + expect(tooltipElement?.innerHTML).toBe( + '
Add your text to this tooltip
' + ); + // Angular informs about stripping some content during sanitization + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining('sanitizing HTML stripped some content') + ); + + divElement.triggerEventHandler('mouseleave', null); + maliciousComponentFixture.detectChanges(); + + tick(500); + + expect(document.body.querySelector('.cps-tooltip')).toBeFalsy(); + })); + + it('should properly show legit tooltip', fakeAsync(() => { + const divElement = legitComponentFixture.debugElement.query(By.css('div')); + divElement.triggerEventHandler('mouseenter', null); + legitComponentFixture.detectChanges(); + + tick(300); + + const tooltipElement: HTMLElement | null = + document.body.querySelector('.cps-tooltip'); + + expect(tooltipElement).toBeTruthy(); + expect(tooltipElement?.innerHTML).toBe( + '

Legit tooltip

' + ); + + divElement.triggerEventHandler('mouseleave', null); + legitComponentFixture.detectChanges(); + + tick(500); + + expect(document.body.querySelector('.cps-tooltip')).toBeFalsy(); + })); +}); diff --git a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts index 8fac7bd9..828e9d17 100644 --- a/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts +++ b/projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts @@ -2,12 +2,14 @@ import { Directive, ElementRef, HostListener, - Inject, + inject, Input, - OnDestroy + OnDestroy, + SecurityContext } from '@angular/core'; import { convertSize } from '../../utils/internal/size-utils'; import { DOCUMENT } from '@angular/common'; +import { DomSanitizer } from '@angular/platform-browser'; /** * CpsTooltipPosition is used to define the position of the tooltip. @@ -96,11 +98,12 @@ export class CpsTooltipDirective implements OnDestroy { private window: Window; - constructor( - private _elementRef: ElementRef, - @Inject(DOCUMENT) private document: Document - ) { - this.window = this.document.defaultView as Window; + private _elementRef = inject(ElementRef); + private _document = inject(DOCUMENT); + private _domSanitizer = inject(DomSanitizer); + + constructor() { + this.window = this._document.defaultView as Window; } ngOnDestroy(): void { @@ -112,7 +115,7 @@ export class CpsTooltipDirective implements OnDestroy { if (this.tooltipDisabled) return; - this._popup = this.document.createElement('div'); + this._popup = this._document.createElement('div'); this._constructElement(this._popup); if (this.tooltipPersistent) @@ -149,15 +152,17 @@ export class CpsTooltipDirective implements OnDestroy { }; private _constructElement(popup: HTMLDivElement) { - const popupContent = this.document.createElement('div'); - popupContent.innerHTML = this.tooltip || 'Add your text to this tooltip'; + const popupContent = this._document.createElement('div'); + popupContent.innerHTML = + this._domSanitizer.sanitize(SecurityContext.HTML, this.tooltip) || + 'Add your text to this tooltip'; popupContent.classList.add(this.tooltipContentClass); popup.appendChild(popupContent); popup.classList.add('cps-tooltip'); popup.style.maxWidth = convertSize(this.tooltipMaxWidth); - this.document.body.appendChild(popup); + this._document.body.appendChild(popup); requestAnimationFrame(function () { popup.style.opacity = '1'; }); diff --git a/tsconfig.json b/tsconfig.json index 707bc6d4..bb75fd94 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,6 @@ "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true - } + }, + "exclude": ["cypress/**/*.ts", "cypress.config.ts"] }