Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spectator): support defer block behavior #641

Merged
merged 4 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
103 changes: 103 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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');
});
});
});