Skip to content

Commit

Permalink
fix(core): properly handling Sanitizer and DomSanitizer #538
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed May 13, 2021
1 parent 8195eeb commit c5c72ae
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 3 deletions.
63 changes: 63 additions & 0 deletions docs/articles/extra/sanitizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Mocking DomSanitizer
description: Information how to test usage of DomSanitizer in Angular
sidebar_label: Mocking DomSanitizer
---

This article explains how to mock `DomSanitizer` in Angular tests properly.

The biggest issue is that `DomSanitizer` are used internally by Angular.
Therefore, mocking them can cause unpredictable errors such as:

- TypeError: view.root.sanitizer.sanitize is not a function
- TypeError: _co.domSanitizer.bypassSecurityTrustHtml is not a function

Another problem is that both of the class is abstract and there is no way to detect their abstract methods in javascript runtime
in order to provide mock functions or spies instead.

However, `ng-mocks` contains [`MockRender`](../api/MockRender.md) which supports additional providers for rendered things.
Therefore, if we use [`MockRender`](../api/MockRender.md) and [`MockProvider`](../api/MockProvider.md), we can achieve desired environment and behavior:

```ts
// rendering TargetComponent component
MockRender(TargetComponent, null, {
// providing special overrides for TargetComponent
providers: [
// Mocking DomSanitizer with a fake method
MockProvider(DomSanitizer, {
// the override should be provided explicitly
// because sanitize method is abstract
sanitize: jasmine.createSpy('sanitize'),
}),
],
});
```

Profit.

## Full example

A full example of **mocking DomSanitizer** in Angular tests.

- [Try it on StackBlitz](https://stackblitz.com/github/ng-mocks/examples?file=src/examples/MockSanitizer/test.spec.ts&initialpath=%3Fspec%3DMockSanitizer)
- [Try it on CodeSandbox](https://codesandbox.io/s/github/ng-mocks/examples?file=/src/examples/MockSanitizer/test.spec.ts&initialpath=%3Fspec%3DMockSanitizer)

```ts
describe('MockSanitizer', () => {
beforeEach(() => MockBuilder(TargetComponent));

it('renders expected mock values', () => {
MockRender(TargetComponent, null, {
providers: [
MockProvider(DomSanitizer, {
sanitize: (context: SecurityContext, value: string) =>
`sanitized:${context}:${value.length}`,
}),
],
});

expect(ngMocks.formatHtml(ngMocks.find('div')))
.toEqual('sanitized:1:23');
});
});
```
27 changes: 26 additions & 1 deletion docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,13 @@ module.exports = {
{
type: 'category',
label: 'Extra',
items: ['extra/customize-mocks', 'extra/mock-observables', 'extra/mock-form-controls', 'extra/with-3rd-party'],
items: [
'extra/customize-mocks',
'extra/mock-observables',
'extra/mock-form-controls',
'extra/sanitizer',
'extra/with-3rd-party',
],
},
{
type: 'category',
Expand Down
39 changes: 39 additions & 0 deletions examples/MockSanitizer/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Component, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import {
MockBuilder,
MockProvider,
MockRender,
ngMocks,
} from 'ng-mocks';

@Component({
selector: 'target',
template: `
<div
[innerHTML]="sanitizer.sanitize(1, '<strong>value1</strong>')"
></div>
`,
})
class TargetComponent {
public constructor(public readonly sanitizer: DomSanitizer) {}
}

describe('MockSanitizer', () => {
beforeEach(() => MockBuilder(TargetComponent));

it('renders expected mock values', () => {
MockRender(TargetComponent, null, {
providers: [
MockProvider(DomSanitizer, {
sanitize: (context: SecurityContext, value: string) =>
`sanitized:${context}:${value.length}`,
}),
],
});

expect(ngMocks.formatHtml(ngMocks.find('div'))).toEqual(
'sanitized:1:23',
);
});
});
4 changes: 4 additions & 0 deletions libs/ng-mocks/src/lib/common/core.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default {
'EventManager',
'Injector', // ivy only
'RendererFactory2',

// https://github.com/ike18t/ng-mocks/issues/538
'Sanitizer',
'DomSanitizer',
],
neverMockToken: [
'InjectionToken Set Injector scope.', // INJECTOR_SCOPE // ivy only
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
const sanitizerMethods = [
'sanitize',
'bypassSecurityTrustHtml',
'bypassSecurityTrustStyle',
'bypassSecurityTrustScript',
'bypassSecurityTrustUrl',
'bypassSecurityTrustResourceUrl',
];

const extraMethods: Record<string, undefined | string[]> = {
DomSanitizer: sanitizerMethods,
Sanitizer: sanitizerMethods,
};

const getOwnPropertyNames = (prototype: any): string[] => {
const result: string[] = Object.getOwnPropertyNames(prototype);
for (const method of extraMethods[prototype.constructor.name] ?? []) {
result.push(method);
}

return result;
};

export default <T>(service: T): string[] => {
const result: string[] = [];

let prototype = service;
while (prototype && Object.getPrototypeOf(prototype) !== null) {
for (const method of Object.getOwnPropertyNames(prototype)) {
for (const method of getOwnPropertyNames(prototype)) {
if ((method as any) === 'constructor') {
continue;
}
Expand Down
71 changes: 71 additions & 0 deletions tests/issue-538/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Component } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import {
MockBuilder,
MockProvider,
MockRender,
ngMocks,
} from 'ng-mocks';

@Component({
selector: 'target',
template: `
<div
[innerHTML]="
domSanitizer.bypassSecurityTrustHtml(
'<strong>value1</strong>'
)
"
></div>
`,
})
class TargetComponent {
public constructor(public readonly domSanitizer: DomSanitizer) {}
}

// TypeError: view.root.sanitizer.sanitize is not a function
describe('issue-538', () => {
describe('keep', () => {
beforeEach(() => MockBuilder(TargetComponent));

it('renders expected real values', () => {
MockRender(TargetComponent);

expect(ngMocks.formatHtml(ngMocks.find('div'))).toEqual(
'<strong>value1</strong>',
);
});
});

describe('mock', () => {
beforeEach(() =>
MockBuilder(TargetComponent).mock(DomSanitizer, {
bypassSecurityTrustHtml: (value: string) => `${value.length}`,
sanitize: (_: any, value: string) => `${value}`,
}),
);

it('renders mess due to internal injections', () => {
MockRender(TargetComponent);

expect(ngMocks.formatHtml(ngMocks.find('div'))).toEqual('23');
});
});

describe('mock-render', () => {
beforeEach(() => MockBuilder(TargetComponent));

it('renders expected mock values', () => {
MockRender(TargetComponent, null, {
providers: [
MockProvider(DomSanitizer, {
bypassSecurityTrustHtml: (value: string) =>
`${value.length}`,
}),
],
});

expect(ngMocks.formatHtml(ngMocks.find('div'))).toEqual('23');
});
});
});

0 comments on commit c5c72ae

Please sign in to comment.