Testing Defer Blocks in Angular with Cypress
Tags: angular, cypress, testing
Published: 2023-12-21 20:00:52

## 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"](https://blog.angular.io/introducing-angular-v17-4d7033312e4b).  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)](https://support.google.com/webmasters/answer/9205520?hl=en) results improving the initial load size and time to paint. 

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

```html
@defer() {
  <p>inside defer</p>
}
```

## The Background
Recently I came across [this issue](https://github.com/cypress-io/cypress/issues/28306) while triaging some issues at Cypress. (Shout out to [MattiaMalandrone](https://github.com/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](https://github.com/jordanpowell88/angular-17-with-defer)

## Get Started
Let's assume we want to test the `AppComponent` which has the following html:

```html
<p>outside defer</p>

@defer() {
<p>inside defer</p>
} @defer(on timer(1000ms)) {
<p>inside defer with condition</p>
}
```

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

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

describe('AppComponent', () => {
  it('can mount', () => {
    cy.mount(AppComponent)
  })
})
```

![Initial App Component Mount](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9rtebcvbu9weup92nse2.png)

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](https://github.com/angular/angular/blob/d315e2c4fa178dfbd41bc25259605bb999fa302e/packages/core/testing/src/defer.ts#L50) 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:

```ts
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));
  }
);
```

Here we added 2 new [Cypress Custom Commands](https://docs.cypress.io/api/cypress-api/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. 

```ts
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');
  });
```

![Finished First Spec](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/097qmf7jji6bkxnxy51j.png)

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

```ts
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');
  });
```

In this example we can chain off our [mount](https://docs.cypress.io/api/commands/mount) command and call our new `defer` command which returns an array of `DeferBlockFixtures`. We can then use [.its](https://docs.cypress.io/api/commands/its#Arrays) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/81ruyu8kiicr6ctn0ad5.png)

Now let's add a test for the second defer block that is tied to a [timer](https://angular.dev/guide/defer#on-timer).

```ts
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');
  });
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4u6npzl8359ufol95qlt.png)

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.

```ts

...

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);
  }
);
```

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

```ts
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');
  });
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nlfa0oclrh77u0773k0m.png)



##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](https://angular.dev/guide/defer#testing) for more details.