Palpatine1991
Palpatine1991

Reputation: 960

Angular unit tests are leaking styles

In our project we have thousands of unit tests and they become slower and slower. I did debug it and found out that the CPU time is spent mostly on the rendering.

After some more debugging I found out that in the Jasmine test page DOM there are thousands of <style> tags which seems to be the core of the performance issue.

I have tried it on the empty ng new project. What I did was just to add some style to the AppComponent:

app.component.css:

.test { color: red; }

When I run the default unit tests (there are 3 pre-defined) using ng test and open the chrome console on the Jasmine test page the result is following - the style is there three times!:

Style issue

While running thousands of tests the performance suffers a lot because of thousands of <style> tags.

Does anybody know how to make karma/angular to cleanup after each test?

I am using angular 6.1.1 and angular/cli 6.0.1

Upvotes: 4

Views: 2813

Answers (4)

Eric Simonton
Eric Simonton

Reputation: 6029

I made a function for this you can use called trimLeftoverStyles, that you can get from my open source library s-ng-dev-utils. It will only remove the styles added by your components; if you added global styles that are used in your test suite it will leave them alone. If you call it in a beforeEach as recommended, it will also leave your component correctly styled at the end of your test, so you can see it correctly for debugging.

Upvotes: 0

Micka&#235;l T.
Micka&#235;l T.

Reputation: 41

I think you hit the following bug: https://github.com/angular/angular/issues/31834

Adding the following line to a global afterEach really speeds up the test suite execution:

window.document.querySelectorAll("style").forEach((style: HTMLStyleElement) => style.remove());

Upvotes: 4

Palpatine1991
Palpatine1991

Reputation: 960

Alright, I have tried more brutal way and it seems it is working. I have created a sample app with 1400 same tests (it just renders @angular/material button and checks it).

It takes 14 minutes to run this test suite.

After adding these lines:

afterEach(() => {
    const head = document.getElementsByTagName('head')[0];

    const styles = document.getElementsByTagName('style');


    for (let i = 0; i < styles.length; i++) {
      head.removeChild(styles[i]);
    }
});

It takes 80 seconds to run it.

It seems to me that Karma should handle this somehow since I bet that this is quite common problem

Upvotes: 3

Reactgular
Reactgular

Reputation: 54781

The component is configured to use ViewEncapsulation.Emulated which means that the app.component.css file should only include a :host style and other host selectors.

Angular will inject the <style> tag for the component and bind it to the host view at run-time. When the component is destroyed the host style is removed, but you've included extra selectors outside the scope of the host. So the browser is leaving those as is, because they were not bound to the host.

Any extra styles that are global should be included via the style.css file that is defined for the project.

The alternative is to use ViewEncapsulation.None and not use a :host selector.

UPDATE:

Angular 6 has added the ViewEncapsulation.ShadowDom setting.

https://w3c.github.io/webcomponents/spec/shadow/

This specification describes a method of combining multiple DOM trees into one hierarchy and how these trees interact with each other within a document, thus enabling better composition of the DOM.

The Angular documents provides an example of:

 styles: [`
    :host {
      display: block;
      border: 1px solid black;
    }
    h1 {
      color: blue;
    }
    .red {
      background-color: red;
    }
  `],

Try using this encapsulation mode instead. Looks like it resolves the issue you've run into.

Upvotes: 1

Related Questions