DEV Community

Jordan Powell for Angular

Posted on • Edited on

Testing Defer Blocks in Angular with Cypress

What is Defer anyways?

Deferrable views are one of the brand new exciting features that shipped as part of Angular 17 last month as part of the new "Angular Renaissance". This feature allows users to defer the loading of select dependencies within an angular component template.

This is important because it allows us to "defer" large components or sections of our code from the initial render of our application. This can improve your application Core Web Vital (CWV) results improving the initial load size and time to paint.

You can see a super trivial example of this in action below:

@defer() {
  <p>inside defer</p>
}
Enter fullscreen mode Exit fullscreen mode

The Background

Recently I came across this issue while triaging some issues at Cypress. (Shout out to MattiaMalandrone for creating an issue with clear instructions for how to reproduce). After quickly replicating the issue I sought after a solution which ultimately inspired me to write this article.

You can download and follow along using this example repo

Get Started

Let's assume we want to test the AppComponent which has the following html:

<p>outside defer</p>

@defer() {
<p>inside defer</p>
} @defer(on timer(1000ms)) {
<p>inside defer with condition</p>
}
Enter fullscreen mode Exit fullscreen mode

To begin we need to create a new file at src/app/app.component.cy.ts where we can write our first test.

import { AppComponent } from './app.component'

describe('AppComponent', () => {
  it('can mount', () => {
    cy.mount(AppComponent)
  })
})
Enter fullscreen mode Exit fullscreen mode

Initial App Component Mount

Though writing our first test was super simple you may notice that none of the blocks of code inside of our @defer() blocks are rendered in the DOM. Thankfully the Angular 17 shipped with some testing utilities that we can utilize to not only gain access to those deferred blocks but also to render them in the DOM.

Next we will add support for both accessing the array of defer blocks and rendering the block in the DOM. Let's open our cypress/support/component.ts file and add the following code:

import { DeferBlockFixture, DeferBlockState } from '@angular/core/testing'

import { MountResponse, mount } from 'cypress/angular';

declare global {
  namespace Cypress {
    interface Chainable {
      mount: typeof mount;
      defer(): Cypress.Chainable<DeferBlockFixture[]>;
      render(state: DeferBlockState): Cypress.Chainable<void>
    }
  }
}

type MountParams = Parameters<typeof mount>;

Cypress.Commands.add('mount', mount);
Cypress.Commands.add(
  'defer',
  { prevSubject: true },
  (subject: MountResponse<MountParams>) => {
    const { fixture } = subject;
    return cy.wrap(fixture.getDeferBlocks());
  }
);

Cypress.Commands.add(
  'render',
  { prevSubject: true },
  (subject: DeferBlockFixture, state: DeferBlockState) => {
    cy.wrap(subject.render(state));
  }
);
Enter fullscreen mode Exit fullscreen mode

Here we added 2 new Cypress Custom Commands defer and render which will allow us to test our blocks of code that use defer(). Now that we have our new Cypress global commands setup we can revisit our spec file to finish adding tests for the uncovered scenarios.

Let's first update our first test to validate that we see the outside defer by default and that we do NOT see the other 2 paragraphs wrapped inside the defer blocks.

it('can mount', () => {
    cy.mount(AppComponent);
    cy.contains('p', 'outside defer');
    cy.contains('p', 'inside defer').should('not.exist');
    cy.contains('p', 'inside defer with condition').should('not.exist');
  });
Enter fullscreen mode Exit fullscreen mode

Finished First Spec

Now let's add tests for the other 2 scenarios using our new custom commands.

it('renders inside the defer block', () => {
    cy.mount(AppComponent).defer().its(0).render(DeferBlockState.Complete);
    cy.contains('p', 'outside defer');
    cy.contains('p', 'inside defer');
  });
Enter fullscreen mode Exit fullscreen mode

In this example we can chain off our mount command and call our new defer command which returns an array of DeferBlockFixtures. We can then use .its to select a specific item from the array and call our second command render with the appropriate DeferBlockState we want to trigger. In our use-case we will want to use DeferBlockState.Complete.

You should now see both the "outside defer" paragraph and the "inside defer" paragraph in our test.

First 2 Specs

Now let's add a test for the second defer block that is tied to a timer.

it('renders inside the defer block with a condition', () => {
  cy.mount(AppComponent).defer().its(1).render(DeferBlockState.Complete);
    cy.contains('p', 'outside defer');
    cy.contains('p', 'inside defer with condition');
  });
Enter fullscreen mode Exit fullscreen mode

Notice the only real difference here is that we are grabbing the second item from the list of deferred views and running the checks on it.

First 3 Specs

Finally let's create one more command in our support file that will render all the defer blocks automatically so we don't have to manually trigger a render for each defer block.


...

declare global {
  namespace Cypress {
    interface Chainable {
      mount: typeof mount;
      defer(): Cypress.Chainable<DeferBlockFixture[]>;
      render(state: DeferBlockState): Cypress.Chainable<void>;
      renderAll(state: DeferBlockState): Cypress.Chainable<DeferBlockFixture[]>;
    }
  }
}

...

Cypress.Commands.add(
  'renderAll',
  { prevSubject: true },
  (subject: DeferBlockFixture[], state: DeferBlockState) => {
    subject.forEach((deferBlock: DeferBlockFixture) => {
      deferBlock.render(state);
    });

    cy.wrap(subject);
  }
);
Enter fullscreen mode Exit fullscreen mode

Now let's add a final test case which validates that all the content in our component is rendered including all defer blocks.

it('renders all defer blocks using renderAll()', () => {
    cy.mount(AppComponent).defer().renderAll(DeferBlockState.Complete);
    cy.contains('p', 'outside defer');
    cy.contains('p', 'inside defer');
    cy.contains('p', 'inside defer with condition');
  });
Enter fullscreen mode Exit fullscreen mode

Now we will see our final test validating that the content outside of the defer blocks AND BOTH defer blocks is rendered successfully in the DOM!

Final State

Conclusion

As you can see that testing defer with Cypress Component Testing is super simple with just a few simple commands. I am really just scratching the surface in this example but feel free to view the official documentation from Angular on how to test defer blocks for more details.

Top comments (10)

Collapse
 
fyodorio profile image
Fyodor

That was fast… my hands are still dirty with fixin cypress after the previous nx/angular update 😅

Collapse
 
mattiamalandrone profile image
Mattia Malandrone

Thanks Jordan!!!!

Collapse
 
psrebrny profile image
Paweł Srebrny

Great article and nice workaround :), but I have some questions:

  • complete defer in the loop is inconvenient because you have to write and index, is there a way to complete all?
  • complete defer after an action is inconvenient but possible :)
  • complete nested defers are impossible is there a way to complete it? The best solution would be to complete all without an index

I know this is a workaround and I would like to use it but for now, it is impossible for me, and I have to still use my workaround from topic github.com/cypress-io/cypress/issu... - and waiting for real implementation in cypress engine

Collapse
 
jordanpowell88 profile image
Jordan Powell

This would be super easy to implement. Might make an update to my example repo if I get time.

Collapse
 
jordanpowell88 profile image
Jordan Powell

Just updated the article to reflect a .renderAll() option

Collapse
 
psrebrny profile image
Paweł Srebrny

Doesn't work on electron and chrome headless too :(

Collapse
 
sraveshnandan profile image
Sravesh Nandan

hi

Collapse
 
codercatdev profile image
Alex Patterson

Deferring the madness ;)

Collapse
 
jangelodev profile image
João Angelo

Thanks Jordan,
This helped me with my project

Collapse
 
jordanpowell88 profile image
Jordan Powell

Love to hear it!