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"]
}