Search code examples
angulard3.jsjasminevegaangular9

Cannot wait until DOM rendering has finished in Angular/Jasmine unit test


I have an Angular pie chart component built through VegaEmbed (https://github.com/vega/vega-embed) which uses Vega and D3 as underlying graphics dependencies. It renders from supplying a title and some (key, value) pairs. I isolated that component, and modified main.ts to run Jasmine out of Stackblitz to share with you. In this test, I am checking that the pie chart renders indeed SVG <text> tags for the values "30%" | "70%" and the legend "Combined CEO/Chair" | "Separate CEO/Chair". However, it seems they run too early and VegaEmbed+Vega+D3 are still busy building that SVG. (I inferred what to test by just looking into the DOM through the Chrome dev tools).

enter image description here

https://stackblitz.com/edit/angular-d3-pie-chart-unit-test

I have tried a range of things: async, FakeAsync + tick, jasmine.clock, changing the promises logic in my Angular component, etc... fixture.whenStable gets me a step closer but texts declared line 50 is still undefined.

I don't know how the internals of Vega, VegaEmbed and D3 are working. If these libraries are not using promises, rather old-fashioned callbacks, then Angular's Zones might fail to wait enough within async ?

What confuses me a little is that console.log(texts); eventually shows a collection of 4 text SVG element in the console. Yet console.log(texts.length); displays 0!

  1. How can that be ?
  2. How do I get my test code to wait till the moment that D3 has finished drawing the SVG and only run expect statements then ?

Solution

  • This is a good question, I have similar issues with Ag-Grid where I have to wait for the rendering or its callbacks to complete before I do assertions and there is no good way like you mentioned with fakeAsync, async/done, etc. At least none that I have found.

    A way I have found is to make a utility function like so:

    import { interval } from 'rxjs';
    .....
    export const waitUntil = async (untilTruthy: Function): Promise<boolean> => {
      while (!untilTruthy()) {
        // older API
        // await interval(25).pipe(take(1)).toPromise();
        // newer API
        await firstValueFrom(interval(25));
      }
      return Promise.resolve(true);
    };
    

    waitUntil will keep looping every 25ms until the callback function supplied is truthy. The amount of time is up to you.

    So in your tests, you can do something like:

    it('should render the chart', async () => {
      // make your arrangements
      // do your action
      fixture.detectChanges();
      // wait for promises to resolve (optional)
      await fixture.whenStable();
      await waitUntil(() => /* put a condition here that will resolve to a truthy value 
      at a later time where the rest of the assertions rely on 
      it such as the graph being present with its labels*/);
      // the rest of your assertions of what should be there what should not
    });
    

    You mention setTimeout working with a value of 0. This works because we are putting what's inside of the setTimeout at the end of the call stack queue because it runs asynchronously. Doing it this way is still good but I like how the tests read with the waitUntil approach.