Skip to content

Commit

Permalink
feat(spectator): Support input signals (#638)
Browse files Browse the repository at this point in the history
* feat(spectator): wip implementation for input signals support

* feat: support input signals

BREAKING CHANGE: remove 'props' and 'setProps' from host/directive
factories

* fix: infer input signals when setting inputs on createComponent

* test: align signal input test between jasmine/jest

* refactor: rename input signal types to be aligned

Fixed type names of InferSignalInputs and InputSignalInput (which was a
typo) to be aligned with Angular's InputSignal type, rather than
SignalInput

* refactor: remove unnecessary generic params from factories

* fix: setprops behaviour for pipe factory

* docs: update documentation to reflect api Changes

Added disclaimers for createHostFactory, createDirectiveFactory, and
createPipeFactory as they don't support props/setProps anymore.
  • Loading branch information
kfrancois committed Feb 17, 2024
1 parent 619ad2b commit 3f63c68
Show file tree
Hide file tree
Showing 39 changed files with 1,701 additions and 1,471 deletions.
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -592,6 +592,9 @@ The host method returns an instance of `SpectatorHost` which extends `Spectator`
- `queryHost` - Read more about querying in Spectator
- `queryHostAll` - Read more about querying in Spectator

Setting inputs directly on a component using `setInput` or `props` is not possible when testing with a host component.
Inputs should be set through `hostProps` or `setHostInput` instead, and passed through to your component in the template.

### Custom Host Component
Sometimes it's helpful to pass your own host implementation. We can pass a custom host component to the `createHostFactory()` that will replace the default one:

Expand Down Expand Up @@ -791,6 +794,9 @@ describe('HighlightDirective', () => {
});
```

Setting inputs directly on a directive using `setInput` or `props` is not possible.
Inputs should be set through `hostProps` or `setHostInput` instead, and passed through to your directive in the template.

## Testing Services

The following example shows how to test a service with Spectator:
Expand Down Expand Up @@ -910,6 +916,10 @@ The `createPipe()` function returns `SpectatorPipe` with the following propertie
- `detectChanges()` - A proxy for Angular `TestBed.fixture.detectChanges()`
- `inject()` - A proxy for Angular `TestBed.inject()`

Setting inputs directly on a pipe using `setInput` or `props` is not possible.
Inputs should be set through `hostProps` or `setHostInput` instead, and passed through to your pipe in the template.


### Using Custom Host Component

The following example illustrates how to test a pipe using a custom host component:
Expand Down
42 changes: 21 additions & 21 deletions package.json
Expand Up @@ -31,25 +31,25 @@
},
"devDependencies": {
"@angular-builders/jest": "^17.0.0",
"@angular-devkit/build-angular": "^17.0.6",
"@angular-devkit/schematics": "^17.0.6",
"@angular-eslint/builder": "17.1.1",
"@angular-eslint/eslint-plugin": "17.1.1",
"@angular-eslint/eslint-plugin-template": "17.1.1",
"@angular-eslint/schematics": "17.1.1",
"@angular-eslint/template-parser": "17.1.1",
"@angular/animations": "^17.0.6",
"@angular/cdk": "^17.0.3",
"@angular/cli": "^17.0.6",
"@angular/common": "^17.0.6",
"@angular/compiler": "^17.0.6",
"@angular/compiler-cli": "^17.0.6",
"@angular/core": "^17.0.6",
"@angular/forms": "^17.0.6",
"@angular/language-service": "^17.0.6",
"@angular/platform-browser": "^17.0.6",
"@angular/platform-browser-dynamic": "^17.0.6",
"@angular/router": "17.0.6",
"@angular-devkit/build-angular": "^17.1.1",
"@angular-devkit/schematics": "^17.1.1",
"@angular-eslint/builder": "17.2.1",
"@angular-eslint/eslint-plugin": "17.2.1",
"@angular-eslint/eslint-plugin-template": "17.2.1",
"@angular-eslint/schematics": "17.2.1",
"@angular-eslint/template-parser": "17.2.1",
"@angular/animations": "^17.1.1",
"@angular/cdk": "^17.1.1",
"@angular/cli": "^17.1.1",
"@angular/common": "^17.1.1",
"@angular/compiler": "^17.1.1",
"@angular/compiler-cli": "^17.1.1",
"@angular/core": "^17.1.0",
"@angular/forms": "^17.1.1",
"@angular/language-service": "^17.1.1",
"@angular/platform-browser": "^17.1.1",
"@angular/platform-browser-dynamic": "^17.1.1",
"@angular/router": "17.1.1",
"@commitlint/cli": "17.3.0",
"@commitlint/config-angular": "17.3.0",
"@commitlint/config-conventional": "17.3.0",
Expand All @@ -69,7 +69,7 @@
"jasmine-core": "5.1.1",
"jasmine-spec-reporter": "7.0.0",
"jest": "29.7.0",
"jest-preset-angular": "13.1.4",
"jest-preset-angular": "14.0.2",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-coverage-istanbul-reporter": "3.0.3",
Expand All @@ -83,7 +83,7 @@
"ts-node": "10.1.0",
"tslib": "^2.6.2",
"typescript": "5.2.2",
"zone.js": "0.14.2"
"zone.js": "0.14.3"
},
"config": {
"commitizen": {
Expand Down
12 changes: 6 additions & 6 deletions projects/spectator/jest/src/lib/spectator-directive.ts
@@ -1,12 +1,12 @@
import { Type } from '@angular/core';
import {
createDirectiveFactory as baseCreateDirectiveFactory,
isType,
HostComponent,
SpectatorDirective as BaseSpectatorDirective,
HostComponent,
isType,
SpectatorDirectiveOptions,
SpectatorDirectiveOverrides,
Token
Token,
} from '@ngneat/spectator';

import { mockProvider, SpyObject } from './mock';
Expand All @@ -25,15 +25,15 @@ export class SpectatorDirective<D, H = HostComponent> extends BaseSpectatorDirec
*/
export type SpectatorDirectiveFactory<D, H = HostComponent> = <HP>(
template: string,
overrides?: SpectatorDirectiveOverrides<D, H, HP>
overrides?: SpectatorDirectiveOverrides<H, HP>
) => SpectatorDirective<D, H & HostComponent extends H ? HP : unknown>;

/**
* @publicApi
*/
export type PresetSpectatorDirectiveFactory<D, H> = <HP>(
template?: string,
overrides?: SpectatorDirectiveOverrides<D, H, HP>
overrides?: SpectatorDirectiveOverrides<H, HP>
) => SpectatorDirective<D, H & HostComponent extends H ? HP : unknown>;

/**
Expand All @@ -53,6 +53,6 @@ export function createDirectiveFactory<D, H = HostComponent>(
): SpectatorDirectiveFactory<D, H> {
return baseCreateDirectiveFactory({
mockProvider,
...(isType(typeOrOptions) ? { directive: typeOrOptions } : typeOrOptions)
...(isType(typeOrOptions) ? { directive: typeOrOptions } : typeOrOptions),
}) as SpectatorDirectiveFactory<D, H>;
}
12 changes: 6 additions & 6 deletions projects/spectator/jest/src/lib/spectator-host.ts
@@ -1,12 +1,12 @@
import { Type } from '@angular/core';
import {
createHostFactory as baseCreateHostFactory,
isType,
HostComponent,
SpectatorHost as BaseSpectatorHost,
HostComponent,
isType,
SpectatorHostOptions,
SpectatorHostOverrides,
Token
Token,
} from '@ngneat/spectator';

import { mockProvider, SpyObject } from './mock';
Expand All @@ -25,15 +25,15 @@ export class SpectatorHost<C, H = HostComponent> extends BaseSpectatorHost<C, H>
*/
export type SpectatorHostFactory<C, H> = <HP>(
template: string,
overrides?: SpectatorHostOverrides<C, H, HP>
overrides?: SpectatorHostOverrides<H, HP>
) => SpectatorHost<C, H & HostComponent extends H ? HP : unknown>;

/**
* @publicApi
*/
export type PresetSpectatorHostFactory<C, H> = <HP>(
template?: string,
overrides?: SpectatorHostOverrides<C, H, HP>
overrides?: SpectatorHostOverrides<H, HP>
) => SpectatorHost<C, H & (HostComponent extends H ? HP : unknown)>;

/**
Expand All @@ -49,6 +49,6 @@ export function createHostFactory<C, H = HostComponent>(typeOrOptions: Type<C> |
export function createHostFactory<C, H = HostComponent>(typeOrOptions: Type<C> | SpectatorHostOptions<C, H>): SpectatorHostFactory<C, H> {
return baseCreateHostFactory({
mockProvider,
...(isType(typeOrOptions) ? { component: typeOrOptions } : typeOrOptions)
...(isType(typeOrOptions) ? { component: typeOrOptions } : typeOrOptions),
}) as SpectatorHostFactory<C, H>;
}
@@ -1,5 +1,5 @@
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { fakeAsync } from '@angular/core/testing';
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';

import { AsyncInputComponent } from '../../../test/async-input/async-input.component';

Expand All @@ -15,16 +15,16 @@ describe('ZippyComponent', () => {

it('should not be visible', () => {
host = createHost(`<app-async-input></app-async-input>`);
host.setInput('widgets', '');
host.setHostInput('widgets', '');
expect(host.query('div')).not.toExist();
});

it('should be visible', fakeAsync(() => {
host = createHost(`<app-async-input></app-async-input>`, {
host = createHost(`<app-async-input [widgets]="widgets"></app-async-input>`, {
detectChanges: true,
props: {
widgets: ''
}
hostProps: {
widgets: '',
},
});
host.tick();
host.detectChanges();
Expand Down
4 changes: 2 additions & 2 deletions projects/spectator/jest/test/auto-focus.directive.spec.ts
Expand Up @@ -13,7 +13,7 @@ describe('DatoAutoFocusDirective', () => {

const createHost = createHostFactory({
component: AutoFocusDirective,
host: CustomHostComponent
host: CustomHostComponent,
});

it('should be focused', () => {
Expand Down Expand Up @@ -46,7 +46,7 @@ describe('DatoAutoFocusDirective (createHostDirectiveFactory)', () => {

const createHost = createDirectiveFactory({
directive: AutoFocusDirective,
host: CustomHostComponent
host: CustomHostComponent,
});

it('should be focused', () => {
Expand Down
@@ -1,5 +1,5 @@
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { createHostFactory, SpectatorHost, Spectator, createComponentFactory } from '@ngneat/spectator/jest';
import { Spectator, SpectatorHost, createComponentFactory, createHostFactory } from '@ngneat/spectator/jest';

import { FormInputComponent } from '../../../test/form-input/form-input.component';

Expand All @@ -12,16 +12,17 @@ describe('FormInputComponent', () => {
const group = new FormGroup({ name: new FormControl('') });

it('should be defined', () => {
host = createHost(`<app-form-input [enableSubnet]="true"></app-form-input>`, {
host = createHost(`<app-form-input [subnetControl]="subnetControl" [enableSubnet]="enableSubnet"></app-form-input>`, {
detectChanges: true,
props: {
hostProps: {
subnetControl: group,
enableSubnet: true,
},
});

expect(host.component).toBeDefined();
expect(host.query('p')).not.toBeNull();
host.setInput('enableSubnet', false);
host.setHostInput('enableSubnet', false);
expect(host.query('p')).toBeNull();
});
});
Expand All @@ -40,12 +41,11 @@ describe('FormInputComponent', () => {
imports: [ReactiveFormsModule],
});

beforeEach(
() =>
(spectator = createComponent({
props: inputs,
}))
);
beforeEach(() => {
spectator = createComponent({
props: inputs,
});
});

it('should work', () => {
expect(spectator.component).toBeDefined();
Expand Down
12 changes: 6 additions & 6 deletions projects/spectator/jest/test/hello/hello.component.spec.ts
Expand Up @@ -8,11 +8,11 @@ describe('HelloComponent', () => {
const createHost = createHostFactory(HelloComponent);

it('should display the title', () => {
host = createHost(`<hello></hello> `, {
props: {
host = createHost(`<hello [title]="title" [widthRaw]="widthRaw"></hello> `, {
hostProps: {
title: 'some title',
widthRaw: 20
}
widthRaw: 20,
},
});

expect((host.query('div') as HTMLElement).style.width).toBe('20px');
Expand All @@ -31,8 +31,8 @@ describe('HelloComponent', () => {
expect('div h2').toHaveText(' some title ', true);

expect('div h2').toHaveExactText(' some title ');
expect('div h2').toHaveExactText('some title', {trim: true});
expect('div h2').not.toHaveExactText('ome title', {trim: true});
expect('div h2').toHaveExactText('some title', { trim: true });
expect('div h2').not.toHaveExactText('ome title', { trim: true });

expect('div h2').toHaveExactTrimmedText('some title');
expect('div h2').not.toHaveExactTrimmedText('ome title');
Expand Down
8 changes: 4 additions & 4 deletions projects/spectator/jest/test/highlight.directive.spec.ts
Expand Up @@ -15,12 +15,12 @@ describe('HighlightDirective', () => {
host.dispatchMouseEvent(host.element, 'mouseover');

expect(host.element).toHaveStyle({
backgroundColor: 'rgba(0,0,0, 0.1)'
backgroundColor: 'rgba(0,0,0, 0.1)',
});

host.dispatchMouseEvent(host.element, 'mouseout');
expect(host.element).toHaveStyle({
backgroundColor: '#fff'
backgroundColor: '#fff',
});
});
});
Expand All @@ -37,12 +37,12 @@ describe('HighlightDirective (createHostDirectiveFactory)', () => {
host.dispatchMouseEvent(host.element, 'mouseover');

expect(host.element).toHaveStyle({
backgroundColor: 'rgba(0,0,0, 0.1)'
backgroundColor: 'rgba(0,0,0, 0.1)',
});

host.dispatchMouseEvent(host.element, 'mouseout');
expect(host.element).toHaveStyle({
backgroundColor: '#fff'
backgroundColor: '#fff',
});
});
});
@@ -0,0 +1,40 @@
import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator/jest';
import { SignalInputComponent } from '../../../test/signal-input/signal-input.component';

describe('SignalInputComponent', () => {
describe('with Spectator', () => {
let spectator: Spectator<SignalInputComponent>;

const createComponent = createComponentFactory({
component: SignalInputComponent,
});

beforeEach(() => {
spectator = createComponent({ props: { show: true } });
});

it('should render a SignalInputComponent', () => {
expect(spectator.query('#text')).toContainText('Hello');
});
});

describe('with SpectatorHost', () => {
let host: SpectatorHost<SignalInputComponent>;

const createHost = createHostFactory({
component: SignalInputComponent,
shallow: true,
template: `<div><app-signal-input [show]="show"></app-signal-input></div>`,
});

beforeEach(() => {
host = createHost();
});

it('should render a SignalInputComponent', () => {
expect(host.query('#text')).not.toExist();
host.setHostInput({ show: true });
expect(host.query('#text')).toContainText('Hello');
});
});
});
Expand Up @@ -14,7 +14,23 @@ describe('StandalonePipe', () => {
spectator = createPipe();
});

it('should render a execute the StandalonePipe', () => {
it('should render and execute the StandalonePipe', () => {
expect(spectator.element.querySelector('#standalone')).toContainText('This stands alone!');
});
});

describe('with host inputs', () => {
let spectator: SpectatorPipe<StandalonePipe>;

const createPipe = createPipeFactory({
pipe: StandalonePipe,
});

beforeEach(() => {
spectator = createPipe(`<div id="standalone">{{ thisField | standalone }}</div>`, { hostProps: { thisField: 'This' } });
});

it('should render and execute the StandalonePipe', () => {
expect(spectator.element.querySelector('#standalone')).toContainText('This stands alone!');
});
});
Expand Down

0 comments on commit 3f63c68

Please sign in to comment.