Skip to content

Commit

Permalink
feat(spectator): support defer block behavior (#641)
Browse files Browse the repository at this point in the history
* feat(spectator): support defer block behavior

* feat(spectator): improve the deferBlocks API

* feat(spectator): provide the index in the defer block method

* docs(spectator): deferrable views in the readme file
  • Loading branch information
profanis committed Mar 4, 2024
1 parent e6f2a52 commit 030ad64
Show file tree
Hide file tree
Showing 7 changed files with 680 additions and 4 deletions.
103 changes: 103 additions & 0 deletions README.md
Expand Up @@ -71,6 +71,8 @@ Become a bronze sponsor and get your logo on our README on GitHub.
- [Testing Select Elements](#testing-select-elements)
- [Mocking Components](#mocking-components)
- [Testing Single Component/Directive Angular Modules](#testing-single-componentdirective-angular-modules)
- [Deferrable Views](#deferrable-views)
- [Nested Deferrable Views](#nested-deferrable-views)
- [Testing with Host](#testing-with-host)
- [Custom Host Component](#custom-host-component)
- [Testing with Routing](#testing-with-routing)
Expand Down Expand Up @@ -154,6 +156,7 @@ const createComponent = createComponentFactory({
declareComponent: false, // Defaults to true
disableAnimations: false, // Defaults to true
shallow: true, // Defaults to false
deferBlockBehavior: DeferBlockBehavior // Defaults to DeferBlockBehavior.Playthrough
});
```

Expand Down Expand Up @@ -551,7 +554,107 @@ const createDirective = createDirectiveFactory({
});
```

### Deferrable Views
The Spectator provides a convenient API to access the deferrable views (`@defer {}`).

Access the desired defer block using the `spectator.deferBlock(optionalIndex)` method. The `optionalIndex` parameter is optional and allows you to specify the index of the defer block you want to access.

- **Accessing the first defer block**: Simply call `spectator.deferBlock()`.
- **Accessing subsequent defer blocks**: Use the corresponding index as an argument. For example, `spectator.deferBlock(1)` accesses the second block (zero-based indexing).

The `spectator.deferBlock(optionalIndex)` returns four methods for rendering different states of the specified defer block:

- `renderComplete()` - Renders the **complete** state of the defer block.
- `renderPlaceholder()` - Renders the **placeholder** state of the defer block.
- `renderLoading()` - Renders the **loading** state of the defer block.
- `renderError()` - Renders the **error** state of the defer block.

**Example:**

```ts
@Component({
selector: 'app-cmp',
template: `
@defer (on viewport) {
<div>Complete state of the first defer block</div> <!--Parent Complete State-->
} @placeholder {
<div>Placeholder</div>
}
`,
})
class DummyComponent {}

const createComponent = createComponentFactory({
component: DummyComponent,
deferBlockBehavior: DeferBlockBehavior.Manual,
});

it('should render the complete state', async () => {
// Arrange
const spectator = createComponent();

// Act
await spectator.deferBlock().renderComplete();

// Assert
expect(spectator.element.outerHTML).toContain('first defer block');
});
```


#### Nested Deferrable Views

To access states within nested defer blocks, call the `deferBlock` method **chaining** from the returned block state method.

**Example:** Accessing the nested complete state:

```ts
// Assuming `spectator.deferBlock(0).renderComplete()` renders the complete state of the parent defer block
const parentCompleteState = await spectator.deferBlock().renderComplete();

// Access the nested complete state of the parent defer block
const nestedCompleteState = await parentCompleteState.renderComplete().deferBlock();
```

**Complete Example**:

```ts
@Component({
selector: 'app-cmp',
template: `
@defer (on viewport) {
<div>Complete state of the first defer block</div> <!--Parent Complete State-->
@defer {
<div>Complete state of the nested defer block</div> <!--Nested Complete State-->
}
} @placeholder {
<div>Placeholder</div>
}
`,
})
class DummyComponent {}

const createComponent = createComponentFactory({
component: DummyComponent,
deferBlockBehavior: DeferBlockBehavior.Manual,
});

it('should render the first nested complete state', async () => {
// Arrange
const spectator = createComponent();

// Act
// Renders the parent complete state
const parentCompleteState = await spectator.deferBlock().renderComplete();

// Renders the nested complete state
await parentCompleteState.deferBlock().renderComplete();

// Assert
expect(spectator.element.outerHTML).toContain('nested defer block');
});
```

## Testing with Host
Testing a component with a host component is a more elegant and powerful technique to test your component.
Expand Down
259 changes: 259 additions & 0 deletions projects/spectator/jest/test/defer-block.spec.ts
@@ -0,0 +1,259 @@
import { Component } from '@angular/core';
import { DeferBlockBehavior, fakeAsync } from '@angular/core/testing';
import { createComponentFactory } from '@ngneat/spectator/jest';

describe('DeferBlock', () => {
describe('Playthrough Behavior', () => {
@Component({
selector: 'app-root',
template: `
<button data-test="button--isVisible" (click)="isVisible = !isVisible">Toggle</button>
@defer (when isVisible) {
<div>empty defer block</div>
} ,
`,
standalone: true,
})
class DummyComponent {
isVisible = false;
}

const createComponent = createComponentFactory({
component: DummyComponent,
deferBlockBehavior: DeferBlockBehavior.Playthrough,
});

it('should render the defer block when isVisible is true', fakeAsync(() => {
// Arrange
const spectator = createComponent();

const button = spectator.query('[data-test="button--isVisible"]')!;

// Act
spectator.click(button);
spectator.tick();
spectator.detectChanges();

// Assert
expect(spectator.element.outerHTML).toContain('empty defer block');
}));
});

describe('Manual Behavior', () => {
@Component({
selector: 'app-root',
template: `
@defer (on viewport) {
<div>empty defer block</div>
} @placeholder {
<div>this is the placeholder text</div>
} @loading {
<div>this is the loading text</div>
} @error {
<div>this is the error text</div>
}
`,
})
class DummyComponent {}

const createComponent = createComponentFactory({
component: DummyComponent,
deferBlockBehavior: DeferBlockBehavior.Manual,
});

it('should render the complete state', async () => {
// Arrange
const spectator = createComponent();

// Act
await spectator.deferBlock().renderComplete();

// Assert
expect(spectator.element.outerHTML).toContain('empty defer block');
});

it('should render the placeholder state', async () => {
// Arrange
const spectator = createComponent();

// Act
await spectator.deferBlock().renderPlaceholder();

// Assert
expect(spectator.element.outerHTML).toContain('this is the placeholder text');
});

it('should render the loading state', async () => {
// Arrange
const spectator = createComponent();

// Act
await spectator.deferBlock().renderLoading();

// Assert
expect(spectator.element.outerHTML).toContain('this is the loading text');
});

it('should render the error state', async () => {
// Arrange
const spectator = createComponent();

// Act
await spectator.deferBlock().renderError();

// Assert
expect(spectator.element.outerHTML).toContain('this is the error text');
});
});

describe('Manual Behavior with nested states', () => {
@Component({
selector: 'app-root',
template: `
@defer (on viewport) {
<div>complete state #1</div>
<!-- nested defer block -->
@defer {
<div>complete state #1.1</div>
<!-- Deep nested defer block #1 -->
@defer {
<div>complete state #1.1.1</div>
} @placeholder {
<div>placeholder state #1.1.1</div>
}
<!-- /Deep nested defer block #1-->
<!-- Deep nested defer block #2 -->
@defer {
<div>complete state #1.1.2</div>
} @placeholder {
<div>placeholder state #1.1.2</div>
}
<!-- /Deep nested defer block #2-->
} @placeholder {
<div>nested placeholder text</div>
} @loading {
<div>nested loading text</div>
} @error {
<div>nested error text</div>
}
<!-- /nested defer block -->
} @placeholder {
<div>placeholder state #1</div>
} @loading {
<div>loading state #1</div>
} @error {
<div>error state #1</div>
}
`,
})
class DummyComponent {}

const createComponent = createComponentFactory({
component: DummyComponent,
deferBlockBehavior: DeferBlockBehavior.Manual,
});

it('should render the first nested complete state', async () => {
// Arrange
const spectator = createComponent();

// Act
const parentCompleteState = await spectator.deferBlock().renderComplete();
await parentCompleteState.deferBlock().renderComplete();

// Assert
expect(spectator.element.outerHTML).toContain('complete state #1.1');
});

it('should render the first deep nested complete state', async () => {
// Arrange
const spectator = createComponent();

// Act
const parentCompleteState = await spectator.deferBlock().renderComplete();
const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete();
await childrenCompleteState.deferBlock().renderComplete();

// Assert
expect(spectator.element.outerHTML).toContain('complete state #1.1.1');
});

it('should render the first deep nested placeholder state', async () => {
// Arrange
const spectator = createComponent();

// Act
const parentCompleteState = await spectator.deferBlock().renderComplete();
const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete();
await childrenCompleteState.deferBlock().renderPlaceholder();

// Assert
expect(spectator.element.outerHTML).toContain('placeholder state #1.1.1');
});

it('should render the second nested complete state', async () => {
// Arrange
const spectator = createComponent();

// Act
const parentCompleteState = await spectator.deferBlock().renderComplete();
const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete();
await childrenCompleteState.deferBlock(1).renderComplete();

// Assert
expect(spectator.element.outerHTML).toContain('complete state #1.1.2');
});

it('should render the second nested placeholder state', async () => {
// Arrange
const spectator = createComponent();

// Act
const parentCompleteState = await spectator.deferBlock().renderComplete();
const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete();
await childrenCompleteState.deferBlock(1).renderPlaceholder();

// Assert
expect(spectator.element.outerHTML).toContain('placeholder state #1.1.2');
});

it('should render the placeholder state', async () => {
// Arrange
const spectator = createComponent();

// Act
await spectator.deferBlock().renderPlaceholder();

// Assert
expect(spectator.element.outerHTML).toContain('placeholder state #1');
});

it('should render the loading state', async () => {
// Arrange
const spectator = createComponent();

// Act
await spectator.deferBlock().renderLoading();

// Assert
expect(spectator.element.outerHTML).toContain('loading state #1');
});

it('should render the error state', async () => {
// Arrange
const spectator = createComponent();

// Act
await spectator.deferBlock().renderError();

// Assert
expect(spectator.element.outerHTML).toContain('error state #1');
});
});
});

0 comments on commit 030ad64

Please sign in to comment.