Skip to content

Commit 02ed01d

Browse files
feat(spectator): support for Function-based outputs (#671)
* feat(spectator): support for Function-based outputs * feat(spectator): limit the output keys at compile time to outputs only * fix(spectator): properly infer the type when passing a T argument for the output * chore(spectator): cleanup the output method overloads and used types --------- Co-authored-by: Anatolie Darii <anatolie.darii@uipath.com>
1 parent defdedd commit 02ed01d

File tree

4 files changed

+118
-6
lines changed

4 files changed

+118
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator/jest';
2+
import { FunctionOutputComponent } from '../../../test/function-output/function-output.component';
3+
4+
describe('FunctionOutputComponent', () => {
5+
describe('with Spectator', () => {
6+
let spectator: Spectator<FunctionOutputComponent>;
7+
8+
const createComponent = createComponentFactory({
9+
component: FunctionOutputComponent,
10+
});
11+
12+
beforeEach(() => {
13+
spectator = createComponent();
14+
});
15+
16+
it('should emit the event on button click', () => {
17+
let output;
18+
spectator.output('buttonClick').subscribe((result) => (output = result));
19+
20+
spectator.click('button');
21+
22+
expect(output).toEqual(true);
23+
});
24+
});
25+
26+
describe('with SpectatorHost', () => {
27+
let host: SpectatorHost<FunctionOutputComponent>;
28+
29+
const createHost = createHostFactory({
30+
component: FunctionOutputComponent,
31+
template: `<app-function-output/>`,
32+
});
33+
34+
beforeEach(() => {
35+
host = createHost();
36+
});
37+
38+
it('should emit the event on button click', () => {
39+
let output;
40+
host.output('buttonClick').subscribe((result) => (output = result));
41+
42+
host.click('button');
43+
44+
expect(output).toEqual(true);
45+
});
46+
});
47+
});

projects/spectator/src/lib/base/dom-spectator.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DebugElement, ElementRef, EventEmitter, Type } from '@angular/core';
1+
import { DebugElement, ElementRef, EventEmitter, OutputEmitterRef, Type } from '@angular/core';
22
import { ComponentFixture, tick } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { Observable } from 'rxjs';
@@ -18,6 +18,11 @@ import { BaseSpectator } from './base-spectator';
1818

1919
const KEY_UP = 'keyup';
2020

21+
type KeysMatchingReturnType<T, V> = keyof { [P in keyof T as T[P] extends V ? P : never]: P } & keyof T;
22+
type KeysMatchingOutputFunction<T> = KeysMatchingReturnType<T, OutputEmitterRef<any>>;
23+
type KeysMatchingClassicOutput<T> = KeysMatchingReturnType<T, EventEmitter<any>>;
24+
type KeysMatchingOutput<T> = KeysMatchingOutputFunction<T> | KeysMatchingClassicOutput<T>;
25+
2126
/**
2227
* @internal
2328
*/
@@ -159,14 +164,17 @@ export abstract class DomSpectator<I> extends BaseSpectator {
159164
return null;
160165
}
161166

162-
public output<T, K extends keyof I = keyof I>(output: K): Observable<T> {
163-
const observable = this.instance[output];
167+
public output<K extends KeysMatchingOutput<I> = KeysMatchingOutput<I>>(output: K): I[K];
168+
public output<T, K extends KeysMatchingClassicOutput<I> = KeysMatchingClassicOutput<I>>(output: K): Observable<T>;
169+
public output<T, K extends KeysMatchingOutputFunction<I> = KeysMatchingOutputFunction<I>>(output: K): OutputEmitterRef<T>;
170+
public output<T, K extends KeysMatchingOutput<I>>(output: K): I[K] | Observable<T> | OutputEmitterRef<T> {
171+
const eventEmitter = this.instance[output];
164172

165-
if (!(observable instanceof Observable)) {
166-
throw new Error(`${String(output)} is not an @Output`);
173+
if (!(eventEmitter instanceof Observable) && !(eventEmitter instanceof OutputEmitterRef)) {
174+
throw new Error(`${String(output)} is not an @Output or an output function`);
167175
}
168176

169-
return observable as Observable<T>;
177+
return eventEmitter;
170178
}
171179

172180
public tick(millis?: number): void {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator';
2+
import { FunctionOutputComponent } from './function-output.component';
3+
4+
describe('FunctionOutputComponent', () => {
5+
describe('with Spectator', () => {
6+
let spectator: Spectator<FunctionOutputComponent>;
7+
8+
const createComponent = createComponentFactory({
9+
component: FunctionOutputComponent,
10+
});
11+
12+
beforeEach(() => {
13+
spectator = createComponent();
14+
});
15+
16+
it('should emit the event on button click', () => {
17+
let output;
18+
spectator.output('buttonClick').subscribe((result) => (output = result));
19+
20+
spectator.click('button');
21+
22+
expect(output).toEqual(true);
23+
});
24+
});
25+
26+
describe('with SpectatorHost', () => {
27+
let host: SpectatorHost<FunctionOutputComponent>;
28+
29+
const createHost = createHostFactory({
30+
component: FunctionOutputComponent,
31+
template: `<app-function-output/>`,
32+
});
33+
34+
beforeEach(() => {
35+
host = createHost();
36+
});
37+
38+
it('should emit the event on button click', () => {
39+
let output;
40+
host.output('buttonClick').subscribe((result) => (output = result));
41+
42+
host.click('button');
43+
44+
expect(output).toEqual(true);
45+
});
46+
});
47+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Component, input, output, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-function-output',
5+
template: ` <button (click)="buttonClick.emit(true)">Emit function output</button> `,
6+
standalone: true,
7+
})
8+
export class FunctionOutputComponent {
9+
public buttonClick = output<boolean>();
10+
}

0 commit comments

Comments
 (0)