Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"types": ["cypress", "node"],
"resolveJsonModule": true
},
"include": ["**/*.ts"]
"include": ["**/*.ts", "../cypress.config.ts"]
}
Original file line number Diff line number Diff line change
@@ -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: `<div cpsTooltip="<style onload='alert(420);'></style>"></div>`,
imports: [CpsTooltipDirective]
})
class MaliciousTooltipComponent {}

@Component({
template: `<div cpsTooltip="<h1>Legit tooltip</h1>"></div>`,
imports: [CpsTooltipDirective]
})
class LegitTooltipComponent {}

describe('CpsTooltipDirective', () => {
let legitComponent: LegitTooltipComponent;
let legitComponentFixture: ComponentFixture<LegitTooltipComponent>;

let maliciousComponent: MaliciousTooltipComponent;
let maliciousComponentFixture: ComponentFixture<MaliciousTooltipComponent>;

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(
'<div class="cps-tooltip-content">Add your text to this tooltip</div>'
);
// 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(
'<div class="cps-tooltip-content"><h1>Legit tooltip</h1></div>'
);

divElement.triggerEventHandler('mouseleave', null);
legitComponentFixture.detectChanges();

tick(500);

expect(document.body.querySelector('.cps-tooltip')).toBeFalsy();
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -96,11 +98,12 @@ export class CpsTooltipDirective implements OnDestroy {

private window: Window;

constructor(
private _elementRef: ElementRef<HTMLElement>,
@Inject(DOCUMENT) private document: Document
) {
this.window = this.document.defaultView as Window;
private _elementRef = inject(ElementRef<HTMLElement>);
private _document = inject(DOCUMENT);
private _domSanitizer = inject(DomSanitizer);

constructor() {
this.window = this._document.defaultView as Window;
}

ngOnDestroy(): void {
Expand All @@ -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)
Expand Down Expand Up @@ -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';
});
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
},
"exclude": ["cypress/**/*.ts", "cypress.config.ts"]
}
Loading